1/*
2 * Copyright (C) 2013 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.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.ImageFormat;
27import android.graphics.PixelFormat;
28import android.graphics.Point;
29import android.graphics.Rect;
30import android.graphics.SurfaceTexture;
31import android.graphics.YuvImage;
32import android.hardware.Camera.Parameters;
33import android.hardware.Camera.Size;
34import android.location.Location;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Handler;
38import android.os.Message;
39import android.os.PowerManager;
40import android.util.Log;
41import android.view.KeyEvent;
42import android.view.OrientationEventListener;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.WindowManager;
46
47import com.android.camera.CameraManager.CameraProxy;
48import com.android.camera.app.OrientationManager;
49import com.android.camera.data.LocalData;
50import com.android.camera.exif.ExifInterface;
51import com.android.camera.util.CameraUtil;
52import com.android.camera.util.UsageStatistics;
53import com.android.camera2.R;
54
55import java.io.ByteArrayOutputStream;
56import java.io.File;
57import java.io.IOException;
58import java.util.List;
59import java.util.TimeZone;
60
61/**
62 * Activity to handle panorama capturing.
63 */
64public class WideAnglePanoramaModule
65        implements CameraModule, WideAnglePanoramaController,
66        SurfaceTexture.OnFrameAvailableListener {
67
68    public static final int DEFAULT_SWEEP_ANGLE = 160;
69    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
70    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
71
72    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
73    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
74    private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3;
75    private static final int MSG_CLEAR_SCREEN_DELAY = 4;
76    private static final int MSG_RESET_TO_PREVIEW = 5;
77
78    private static final int SCREEN_DELAY = 2 * 60 * 1000;
79
80    @SuppressWarnings("unused")
81    private static final String TAG = "CAM_WidePanoModule";
82    private static final int PREVIEW_STOPPED = 0;
83    private static final int PREVIEW_ACTIVE = 1;
84    public static final int CAPTURE_STATE_VIEWFINDER = 0;
85    public static final int CAPTURE_STATE_MOSAIC = 1;
86
87    // The unit of speed is degrees per frame.
88    private static final float PANNING_SPEED_THRESHOLD = 2.5f;
89    private static final boolean DEBUG = false;
90
91    private ContentResolver mContentResolver;
92    private WideAnglePanoramaUI mUI;
93
94    private MosaicPreviewRenderer mMosaicPreviewRenderer;
95    private Object mRendererLock = new Object();
96    private Object mWaitObject = new Object();
97
98    private String mPreparePreviewString;
99    private String mDialogTitle;
100    private String mDialogOkString;
101    private String mDialogPanoramaFailedString;
102    private String mDialogWaitingPreviousString;
103
104    private int mPreviewUIWidth;
105    private int mPreviewUIHeight;
106    private boolean mUsingFrontCamera;
107    private int mCameraPreviewWidth;
108    private int mCameraPreviewHeight;
109    private int mCameraState;
110    private int mCaptureState;
111    private PowerManager.WakeLock mPartialWakeLock;
112    private MosaicFrameProcessor mMosaicFrameProcessor;
113    private boolean mMosaicFrameProcessorInitialized;
114    private AsyncTask <Void, Void, Void> mWaitProcessorTask;
115    private long mTimeTaken;
116    private Handler mMainHandler;
117    private SurfaceTexture mCameraTexture;
118    private boolean mThreadRunning;
119    private boolean mCancelComputation;
120    private float mHorizontalViewAngle;
121    private float mVerticalViewAngle;
122
123    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
124    // getting a better image quality by the former.
125    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
126
127    private PanoOrientationEventListener mOrientationEventListener;
128    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
129    // respectively.
130    private int mDeviceOrientation;
131    private int mDeviceOrientationAtCapture;
132    private int mCameraOrientation;
133    private int mOrientationCompensation;
134
135    private SoundClips.Player mSoundPlayer;
136
137    private Runnable mOnFrameAvailableRunnable;
138
139    private CameraActivity mActivity;
140    private View mRootView;
141    private CameraProxy mCameraDevice;
142    private boolean mPaused;
143
144    private LocationManager mLocationManager;
145    private OrientationManager mOrientationManager;
146    private ComboPreferences mPreferences;
147    private boolean mMosaicPreviewConfigured;
148    private boolean mPreviewFocused = true;
149
150    @Override
151    public void onPreviewUIReady() {
152        configMosaicPreview();
153    }
154
155    @Override
156    public void onPreviewUIDestroyed() {
157
158    }
159
160    private class MosaicJpeg {
161        public MosaicJpeg(byte[] data, int width, int height) {
162            this.data = data;
163            this.width = width;
164            this.height = height;
165            this.isValid = true;
166        }
167
168        public MosaicJpeg() {
169            this.data = null;
170            this.width = 0;
171            this.height = 0;
172            this.isValid = false;
173        }
174
175        public final byte[] data;
176        public final int width;
177        public final int height;
178        public final boolean isValid;
179    }
180
181    private class PanoOrientationEventListener extends OrientationEventListener {
182        public PanoOrientationEventListener(Context context) {
183            super(context);
184        }
185
186        @Override
187        public void onOrientationChanged(int orientation) {
188            // We keep the last known orientation. So if the user first orient
189            // the camera then point the camera to floor or sky, we still have
190            // the correct orientation.
191            if (orientation == ORIENTATION_UNKNOWN) return;
192            mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation);
193            // When the screen is unlocked, display rotation may change. Always
194            // calculate the up-to-date orientationCompensation.
195            int orientationCompensation = mDeviceOrientation
196                    + CameraUtil.getDisplayRotation(mActivity) % 360;
197            if (mOrientationCompensation != orientationCompensation) {
198                mOrientationCompensation = orientationCompensation;
199            }
200        }
201    }
202
203    @Override
204    public void init(CameraActivity activity, View parent) {
205        mActivity = activity;
206        mRootView = parent;
207
208        mOrientationManager = new OrientationManager(activity);
209        mCaptureState = CAPTURE_STATE_VIEWFINDER;
210        mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView);
211        mUI.setCaptureProgressOnDirectionChangeListener(
212                new PanoProgressBar.OnDirectionChangeListener() {
213                    @Override
214                    public void onDirectionChange(int direction) {
215                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
216                            mUI.showDirectionIndicators(direction);
217                        }
218                    }
219                });
220
221        mContentResolver = mActivity.getContentResolver();
222        // This runs in UI thread.
223        mOnFrameAvailableRunnable = new Runnable() {
224            @Override
225            public void run() {
226                // Frames might still be available after the activity is paused.
227                // If we call onFrameAvailable after pausing, the GL thread will crash.
228                if (mPaused) return;
229
230                MosaicPreviewRenderer renderer = null;
231                synchronized (mRendererLock) {
232                    if (mMosaicPreviewRenderer == null) {
233                        return;
234                    }
235                    renderer = mMosaicPreviewRenderer;
236                }
237                if (mRootView.getVisibility() != View.VISIBLE) {
238                    renderer.showPreviewFrameSync();
239                    mRootView.setVisibility(View.VISIBLE);
240                } else {
241                    if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
242                        renderer.showPreviewFrame();
243                    } else {
244                        renderer.alignFrameSync();
245                        mMosaicFrameProcessor.processFrame();
246                    }
247                }
248            }
249        };
250
251        PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
252        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
253
254        mOrientationEventListener = new PanoOrientationEventListener(mActivity);
255
256        mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
257
258        Resources appRes = mActivity.getResources();
259        mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
260        mDialogTitle = appRes.getString(R.string.pano_dialog_title);
261        mDialogOkString = appRes.getString(R.string.dialog_ok);
262        mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
263        mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
264
265        mPreferences = new ComboPreferences(mActivity);
266        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
267        mLocationManager = new LocationManager(mActivity, null);
268
269        mMainHandler = new Handler() {
270            @Override
271            public void handleMessage(Message msg) {
272                switch (msg.what) {
273                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
274                        onBackgroundThreadFinished();
275                        showFinalMosaic((Bitmap) msg.obj);
276                        saveHighResMosaic();
277                        break;
278                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
279                        onBackgroundThreadFinished();
280                        if (mPaused) {
281                            resetToPreviewIfPossible();
282                        } else {
283                            mUI.showAlertDialog(
284                                    mDialogTitle, mDialogPanoramaFailedString,
285                                    mDialogOkString, new Runnable() {
286                                @Override
287                                public void run() {
288                                    resetToPreviewIfPossible();
289                                }
290                            });
291                        }
292                        clearMosaicFrameProcessorIfNeeded();
293                        break;
294                    case MSG_END_DIALOG_RESET_TO_PREVIEW:
295                        onBackgroundThreadFinished();
296                        resetToPreviewIfPossible();
297                        clearMosaicFrameProcessorIfNeeded();
298                        break;
299                    case MSG_CLEAR_SCREEN_DELAY:
300                        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
301                                FLAG_KEEP_SCREEN_ON);
302                        break;
303                    case MSG_RESET_TO_PREVIEW:
304                        resetToPreviewIfPossible();
305                        break;
306                }
307            }
308        };
309    }
310
311    @Override
312    public void onPreviewFocusChanged(boolean previewFocused) {
313        mPreviewFocused = previewFocused;
314        mUI.onPreviewFocusChanged(previewFocused);
315    }
316
317    @Override
318    public boolean arePreviewControlsVisible() {
319        return mUI.arePreviewControlsVisible();
320    }
321
322    /**
323     * Opens camera and sets the parameters.
324     *
325     * @return Whether the camera was opened successfully.
326     */
327    private boolean setupCamera() {
328        if (!openCamera()) {
329            return false;
330        }
331        Parameters parameters = mCameraDevice.getParameters();
332        setupCaptureParams(parameters);
333        configureCamera(parameters);
334        return true;
335    }
336
337    private void releaseCamera() {
338        if (mCameraDevice != null) {
339            CameraHolder.instance().release();
340            mCameraDevice = null;
341            mCameraState = PREVIEW_STOPPED;
342        }
343    }
344
345    /**
346     * Opens the camera device. The back camera has priority over the front
347     * one.
348     *
349     * @return Whether the camera was opened successfully.
350     */
351    private boolean openCamera() {
352        int cameraId = CameraHolder.instance().getBackCameraId();
353        // If there is no back camera, use the first camera. Camera id starts
354        // from 0. Currently if a camera is not back facing, it is front facing.
355        // This is also forward compatible if we have a new facing other than
356        // back or front in the future.
357        if (cameraId == -1) cameraId = 0;
358        mCameraDevice = CameraUtil.openCamera(mActivity, cameraId,
359                mMainHandler, mActivity.getCameraOpenErrorCallback());
360        if (mCameraDevice == null) {
361            return false;
362        }
363        mCameraOrientation = CameraUtil.getCameraOrientation(cameraId);
364        if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
365        return true;
366    }
367
368    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
369            boolean needSmaller) {
370        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
371        boolean hasFound = false;
372        for (Size size : supportedSizes) {
373            int h = size.height;
374            int w = size.width;
375            // we only want 4:3 format.
376            int d = DEFAULT_CAPTURE_PIXELS - h * w;
377            if (needSmaller && d < 0) { // no bigger preview than 960x720.
378                continue;
379            }
380            if (need4To3 && (h * 4 != w * 3)) {
381                continue;
382            }
383            d = Math.abs(d);
384            if (d < pixelsDiff) {
385                mCameraPreviewWidth = w;
386                mCameraPreviewHeight = h;
387                pixelsDiff = d;
388                hasFound = true;
389            }
390        }
391        return hasFound;
392    }
393
394    private void setupCaptureParams(Parameters parameters) {
395        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
396        if (!findBestPreviewSize(supportedSizes, true, true)) {
397            Log.w(TAG, "No 4:3 ratio preview size supported.");
398            if (!findBestPreviewSize(supportedSizes, false, true)) {
399                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
400                findBestPreviewSize(supportedSizes, false, false);
401            }
402        }
403        Log.d(TAG, "camera preview h = "
404                    + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth);
405        parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight);
406
407        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
408        int last = frameRates.size() - 1;
409        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
410        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
411        parameters.setPreviewFpsRange(minFps, maxFps);
412        Log.d(TAG, "preview fps: " + minFps + ", " + maxFps);
413
414        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
415        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
416            parameters.setFocusMode(mTargetFocusMode);
417        } else {
418            // Use the default focus mode and log a message
419            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
420                  " becuase the mode is not supported.");
421        }
422
423        parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE);
424
425        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
426        mVerticalViewAngle =  parameters.getVerticalViewAngle();
427    }
428
429    public int getPreviewBufSize() {
430        PixelFormat pixelInfo = new PixelFormat();
431        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
432        // TODO: remove this extra 32 byte after the driver bug is fixed.
433        return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
434    }
435
436    private void configureCamera(Parameters parameters) {
437        mCameraDevice.setParameters(parameters);
438    }
439
440    /**
441     * Configures the preview renderer according to the dimension defined by
442     * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}.
443     * Will stop the camera preview first.
444     */
445    private void configMosaicPreview() {
446        if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0
447                || mUI.getSurfaceTexture() == null) {
448            return;
449        }
450
451        stopCameraPreview();
452        synchronized (mRendererLock) {
453            if (mMosaicPreviewRenderer != null) {
454                mMosaicPreviewRenderer.release();
455            }
456            mMosaicPreviewRenderer = null;
457        }
458        final boolean isLandscape =
459                (mActivity.getResources().getConfiguration().orientation ==
460                        Configuration.ORIENTATION_LANDSCAPE);
461        mUI.flipPreviewIfNeeded();
462        MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
463                mUI.getSurfaceTexture(),
464                mPreviewUIWidth, mPreviewUIHeight, isLandscape);
465        synchronized (mRendererLock) {
466            mMosaicPreviewRenderer = renderer;
467            mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
468
469            if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
470                mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
471            }
472            mRendererLock.notifyAll();
473        }
474        mMosaicPreviewConfigured = true;
475        resetToPreviewIfPossible();
476    }
477
478    /**
479     * Receives the layout change event from the preview area. So we can
480     * initialize the mosaic preview renderer.
481     */
482    @Override
483    public void onPreviewUILayoutChange(int l, int t, int r, int b) {
484        Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t));
485        mPreviewUIWidth = r - l;
486        mPreviewUIHeight = b - t;
487        configMosaicPreview();
488    }
489
490    @Override
491    public void onFrameAvailable(SurfaceTexture surface) {
492        /* This function may be called by some random thread,
493         * so let's be safe and jump back to ui thread.
494         * No OpenGL calls can be done here. */
495        mActivity.runOnUiThread(mOnFrameAvailableRunnable);
496    }
497
498    public void startCapture() {
499        // Reset values so we can do this again.
500        mCancelComputation = false;
501        mTimeTaken = System.currentTimeMillis();
502        mActivity.setSwipingEnabled(false);
503        mCaptureState = CAPTURE_STATE_MOSAIC;
504        mUI.onStartCapture();
505
506        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
507            @Override
508            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
509                    float progressX, float progressY) {
510                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
511                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
512                if (isFinished
513                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
514                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
515                    stopCapture(false);
516                } else {
517                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
518                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
519                    mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree,
520                            accumulatedHorizontalAngle, accumulatedVerticalAngle,
521                            PANNING_SPEED_THRESHOLD);
522                }
523            }
524        });
525
526        mUI.resetCaptureProgress();
527        // TODO: calculate the indicator width according to different devices to reflect the actual
528        // angle of view of the camera device.
529        mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE);
530        mUI.showCaptureProgress();
531        mDeviceOrientationAtCapture = mDeviceOrientation;
532        keepScreenOn();
533        // TODO: mActivity.getOrientationManager().lockOrientation();
534        mOrientationManager.lockOrientation();
535        int degrees = CameraUtil.getDisplayRotation(mActivity);
536        int cameraId = CameraHolder.instance().getBackCameraId();
537        int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId);
538        mUI.setProgressOrientation(orientation);
539    }
540
541    private void stopCapture(boolean aborted) {
542        mCaptureState = CAPTURE_STATE_VIEWFINDER;
543        mUI.onStopCapture();
544
545        mMosaicFrameProcessor.setProgressListener(null);
546        stopCameraPreview();
547
548        mCameraTexture.setOnFrameAvailableListener(null);
549
550        if (!aborted && !mThreadRunning) {
551            mUI.showWaitingDialog(mPreparePreviewString);
552            // Hide shutter button, shutter icon, etc when waiting for
553            // panorama to stitch
554            mUI.hideUI();
555            runBackgroundThread(new Thread() {
556                @Override
557                public void run() {
558                    MosaicJpeg jpeg = generateFinalMosaic(false);
559
560                    if (jpeg != null && jpeg.isValid) {
561                        Bitmap bitmap = null;
562                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
563                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
564                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
565                    } else {
566                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
567                                MSG_END_DIALOG_RESET_TO_PREVIEW));
568                    }
569                }
570            });
571        }
572        keepScreenOnAwhile();
573    }
574
575    @Override
576    public void onShutterButtonClick() {
577        // If mCameraTexture == null then GL setup is not finished yet.
578        // No buttons can be pressed.
579        if (mPaused || mThreadRunning || mCameraTexture == null) {
580            return;
581        }
582        // Since this button will stay on the screen when capturing, we need to check the state
583        // right now.
584        switch (mCaptureState) {
585            case CAPTURE_STATE_VIEWFINDER:
586                final long storageSpaceBytes = mActivity.getStorageSpaceBytes();
587                if(storageSpaceBytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
588                    Log.w(TAG, "Low storage warning: " + storageSpaceBytes);
589                    return;
590                }
591                mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
592                startCapture();
593                break;
594            case CAPTURE_STATE_MOSAIC:
595                mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
596                stopCapture(false);
597                break;
598            default:
599                Log.w(TAG, "Unknown capture state: " + mCaptureState);
600                break;
601        }
602    }
603
604    public void reportProgress() {
605        mUI.resetSavingProgress();
606        Thread t = new Thread() {
607            @Override
608            public void run() {
609                while (mThreadRunning) {
610                    final int progress = mMosaicFrameProcessor.reportProgress(
611                            true, mCancelComputation);
612
613                    try {
614                        synchronized (mWaitObject) {
615                            mWaitObject.wait(50);
616                        }
617                    } catch (InterruptedException e) {
618                        throw new RuntimeException("Panorama reportProgress failed", e);
619                    }
620                    // Update the progress bar
621                    mActivity.runOnUiThread(new Runnable() {
622                        @Override
623                        public void run() {
624                            mUI.updateSavingProgress(progress);
625                        }
626                    });
627                }
628            }
629        };
630        t.start();
631    }
632
633    private int getCaptureOrientation() {
634        // The panorama image returned from the library is oriented based on the
635        // natural orientation of a camera. We need to set an orientation for the image
636        // in its EXIF header, so the image can be displayed correctly.
637        // The orientation is calculated from compensating the
638        // device orientation at capture and the camera orientation respective to
639        // the natural orientation of the device.
640        int orientation;
641        if (mUsingFrontCamera) {
642            // mCameraOrientation is negative with respect to the front facing camera.
643            // See document of android.hardware.Camera.Parameters.setRotation.
644            orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
645        } else {
646            orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
647        }
648        return orientation;
649    }
650
651    /** The orientation of the camera image. The value is the angle that the camera
652     *  image needs to be rotated clockwise so it shows correctly on the display
653     *  in its natural orientation. It should be 0, 90, 180, or 270.*/
654    public int getCameraOrientation() {
655        return mCameraOrientation;
656    }
657
658    public void saveHighResMosaic() {
659        runBackgroundThread(new Thread() {
660            @Override
661            public void run() {
662                mPartialWakeLock.acquire();
663                MosaicJpeg jpeg;
664                try {
665                    jpeg = generateFinalMosaic(true);
666                } finally {
667                    mPartialWakeLock.release();
668                }
669
670                if (jpeg == null) {  // Cancelled by user.
671                    mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
672                } else if (!jpeg.isValid) {  // Error when generating mosaic.
673                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
674                } else {
675                    int orientation = getCaptureOrientation();
676                    final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
677                    if (uri != null) {
678                        mActivity.runOnUiThread(new Runnable() {
679                            @Override
680                            public void run() {
681                                mActivity.notifyNewMedia(uri);
682                            }
683                        });
684                    }
685                    mMainHandler.sendMessage(
686                            mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
687                }
688            }
689        });
690        reportProgress();
691    }
692
693    private void runBackgroundThread(Thread thread) {
694        mThreadRunning = true;
695        thread.start();
696    }
697
698    private void onBackgroundThreadFinished() {
699        mThreadRunning = false;
700        mUI.dismissAllDialogs();
701    }
702
703    private void cancelHighResComputation() {
704        mCancelComputation = true;
705        synchronized (mWaitObject) {
706            mWaitObject.notify();
707        }
708    }
709
710    // This function will be called upon the first camera frame is available.
711    private void reset() {
712        mCaptureState = CAPTURE_STATE_VIEWFINDER;
713
714        mOrientationManager.unlockOrientation();
715        mUI.reset();
716        mActivity.setSwipingEnabled(true);
717        // Orientation change will trigger onLayoutChange->configMosaicPreview->
718        // resetToPreview. Do not show the capture UI in film strip.
719        if (mPreviewFocused) {
720            mUI.showPreviewUI();
721        }
722        mMosaicFrameProcessor.reset();
723    }
724
725    private void resetToPreviewIfPossible() {
726        reset();
727        if (!mMosaicFrameProcessorInitialized
728                || mUI.getSurfaceTexture() == null
729                || !mMosaicPreviewConfigured) {
730            return;
731        }
732        if (!mPaused) {
733            startCameraPreview();
734        }
735    }
736
737    private void showFinalMosaic(Bitmap bitmap) {
738        mUI.showFinalMosaic(bitmap, getCaptureOrientation());
739    }
740
741    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
742        if (jpegData != null) {
743            String filename = PanoUtil.createName(
744                    mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
745            String filepath = Storage.generateFilepath(filename);
746
747            UsageStatistics.onEvent(UsageStatistics.COMPONENT_PANORAMA,
748                    UsageStatistics.ACTION_CAPTURE_DONE, null, 0,
749                    UsageStatistics.hashFileName(filename + ".jpg"));
750
751            Location loc = mLocationManager.getCurrentLocation();
752            ExifInterface exif = new ExifInterface();
753            try {
754                exif.readExif(jpegData);
755                exif.addGpsDateTimeStampTag(mTimeTaken);
756                exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken,
757                        TimeZone.getDefault());
758                exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
759                        ExifInterface.getOrientationValueForRotation(orientation)));
760                writeLocation(loc, exif);
761                exif.writeExif(jpegData, filepath);
762            } catch (IOException e) {
763                Log.e(TAG, "Cannot set exif for " + filepath, e);
764                Storage.writeFile(filepath, jpegData);
765            }
766            int jpegLength = (int) (new File(filepath).length());
767            return Storage.addImage(mContentResolver, filename, mTimeTaken, loc, orientation,
768                    jpegLength, filepath, width, height, LocalData.MIME_TYPE_JPEG);
769        }
770        return null;
771    }
772
773    private static void writeLocation(Location location, ExifInterface exif) {
774        if (location == null) {
775            return;
776        }
777        exif.addGpsTags(location.getLatitude(), location.getLongitude());
778        exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()));
779    }
780
781    private void clearMosaicFrameProcessorIfNeeded() {
782        if (!mPaused || mThreadRunning) return;
783        // Only clear the processor if it is initialized by this activity
784        // instance. Other activity instances may be using it.
785        if (mMosaicFrameProcessorInitialized) {
786            mMosaicFrameProcessor.clear();
787            mMosaicFrameProcessorInitialized = false;
788        }
789    }
790
791    private void initMosaicFrameProcessorIfNeeded() {
792        if (mPaused || mThreadRunning) {
793            return;
794        }
795
796        mMosaicFrameProcessor.initialize(
797                mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize());
798        mMosaicFrameProcessorInitialized = true;
799    }
800
801    @Override
802    public void onPauseBeforeSuper() {
803        mPaused = true;
804        if (mLocationManager != null) mLocationManager.recordLocation(false);
805        mOrientationManager.pause();
806    }
807
808    @Override
809    public void onPauseAfterSuper() {
810        mOrientationEventListener.disable();
811        if (mCameraDevice == null) {
812            // Camera open failed. Nothing should be done here.
813            return;
814        }
815        // Stop the capturing first.
816        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
817            stopCapture(true);
818            reset();
819        }
820        mUI.showPreviewCover();
821        releaseCamera();
822        synchronized (mRendererLock) {
823            mCameraTexture = null;
824
825            // The preview renderer might not have a chance to be initialized
826            // before onPause().
827            if (mMosaicPreviewRenderer != null) {
828                mMosaicPreviewRenderer.release();
829                mMosaicPreviewRenderer = null;
830            }
831        }
832
833        clearMosaicFrameProcessorIfNeeded();
834        if (mWaitProcessorTask != null) {
835            mWaitProcessorTask.cancel(true);
836            mWaitProcessorTask = null;
837        }
838        resetScreenOn();
839        mUI.removeDisplayChangeListener();
840        if (mSoundPlayer != null) {
841            mSoundPlayer.release();
842            mSoundPlayer = null;
843        }
844        System.gc();
845    }
846
847    @Override
848    public void onConfigurationChanged(Configuration newConfig) {
849        mUI.onConfigurationChanged(newConfig, mThreadRunning);
850    }
851
852    @Override
853    public void onOrientationChanged(int orientation) {
854    }
855
856    @Override
857    public void onResumeBeforeSuper() {
858        mPaused = false;
859    }
860
861    @Override
862    public void onResumeAfterSuper() {
863        mOrientationEventListener.enable();
864
865        mCaptureState = CAPTURE_STATE_VIEWFINDER;
866
867        if (!setupCamera()) {
868            Log.e(TAG, "Failed to open camera, aborting");
869            return;
870        }
871
872        // Set up sound playback for shutter button
873        mSoundPlayer = SoundClips.getPlayer(mActivity);
874
875        // Check if another panorama instance is using the mosaic frame processor.
876        mUI.dismissAllDialogs();
877        if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
878            mUI.showWaitingDialog(mDialogWaitingPreviousString);
879            // If stitching is still going on, make sure switcher and shutter button
880            // are not showing
881            mUI.hideUI();
882            mWaitProcessorTask = new WaitProcessorTask().execute();
883        } else {
884            // Camera must be initialized before MosaicFrameProcessor is
885            // initialized. The preview size has to be decided by camera device.
886            initMosaicFrameProcessorIfNeeded();
887            Point size = mUI.getPreviewAreaSize();
888            mPreviewUIWidth = size.x;
889            mPreviewUIHeight = size.y;
890            configMosaicPreview();
891            mActivity.updateStorageSpaceAndHint();
892        }
893        keepScreenOnAwhile();
894
895        mOrientationManager.resume();
896        // Initialize location service.
897        boolean recordLocation = RecordLocationPreference.get(mPreferences,
898                mContentResolver);
899        mLocationManager.recordLocation(recordLocation);
900        mUI.initDisplayChangeListener();
901        UsageStatistics.onContentViewChanged(
902                UsageStatistics.COMPONENT_CAMERA, "PanoramaModule");
903    }
904
905    /**
906     * Generate the final mosaic image.
907     *
908     * @param highRes flag to indicate whether we want to get a high-res version.
909     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
910     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
911     *         is an error in generating the final mosaic.
912     */
913    public MosaicJpeg generateFinalMosaic(boolean highRes) {
914        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
915        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
916            return null;
917        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
918            return new MosaicJpeg();
919        }
920
921        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
922        if (imageData == null) {
923            Log.e(TAG, "getFinalMosaicNV21() returned null.");
924            return new MosaicJpeg();
925        }
926
927        int len = imageData.length - 8;
928        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
929                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
930        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
931                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
932        Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
933
934        if (width <= 0 || height <= 0) {
935            // TODO: pop up an error message indicating that the final result is not generated.
936            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
937                    height);
938            return new MosaicJpeg();
939        }
940
941        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
942        ByteArrayOutputStream out = new ByteArrayOutputStream();
943        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
944        try {
945            out.close();
946        } catch (Exception e) {
947            Log.e(TAG, "Exception in storing final mosaic", e);
948            return new MosaicJpeg();
949        }
950        return new MosaicJpeg(out.toByteArray(), width, height);
951    }
952
953    private void startCameraPreview() {
954        if (mCameraDevice == null) {
955            // Camera open failed. Return.
956            return;
957        }
958
959        if (mUI.getSurfaceTexture() == null) {
960            // UI is not ready.
961            return;
962        }
963
964        // This works around a driver issue. startPreview may fail if
965        // stopPreview/setPreviewTexture/startPreview are called several times
966        // in a row. mCameraTexture can be null after pressing home during
967        // mosaic generation and coming back. Preview will be started later in
968        // onLayoutChange->configMosaicPreview. This also reduces the latency.
969        synchronized (mRendererLock) {
970            if (mCameraTexture == null) return;
971
972            // If we're previewing already, stop the preview first (this will
973            // blank the screen).
974            if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
975
976            // Set the display orientation to 0, so that the underlying mosaic
977            // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight
978            // image data from SurfaceTexture.
979            mCameraDevice.setDisplayOrientation(0);
980
981            mCameraTexture.setOnFrameAvailableListener(this);
982            mCameraDevice.setPreviewTexture(mCameraTexture);
983        }
984        mCameraDevice.startPreview();
985        mCameraState = PREVIEW_ACTIVE;
986    }
987
988    private void stopCameraPreview() {
989        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
990            mCameraDevice.stopPreview();
991        }
992        mCameraState = PREVIEW_STOPPED;
993    }
994
995    @Override
996    public void onUserInteraction() {
997        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
998    }
999
1000    @Override
1001    public boolean onBackPressed() {
1002        // If panorama is generating low res or high res mosaic, ignore back
1003        // key. So the activity will not be destroyed.
1004        if (mThreadRunning) return true;
1005        return false;
1006    }
1007
1008    private void resetScreenOn() {
1009        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1010        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1011    }
1012
1013    private void keepScreenOnAwhile() {
1014        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1015        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1016        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1017    }
1018
1019    private void keepScreenOn() {
1020        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1021        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1022    }
1023
1024    private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
1025        @Override
1026        protected Void doInBackground(Void... params) {
1027            synchronized (mMosaicFrameProcessor) {
1028                while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1029                    try {
1030                        mMosaicFrameProcessor.wait();
1031                    } catch (Exception e) {
1032                        // ignore
1033                    }
1034                }
1035            }
1036            mActivity.updateStorageSpace();
1037            return null;
1038        }
1039
1040        @Override
1041        protected void onPostExecute(Void result) {
1042            mWaitProcessorTask = null;
1043            mUI.dismissAllDialogs();
1044            // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE);
1045            initMosaicFrameProcessorIfNeeded();
1046            Point size = mUI.getPreviewAreaSize();
1047            mPreviewUIWidth = size.x;
1048            mPreviewUIHeight = size.y;
1049            configMosaicPreview();
1050            resetToPreviewIfPossible();
1051            mActivity.updateStorageHint(mActivity.getStorageSpaceBytes());
1052        }
1053    }
1054
1055    @Override
1056    public void cancelHighResStitching() {
1057        if (mPaused || mCameraTexture == null) return;
1058        cancelHighResComputation();
1059    }
1060
1061    @Override
1062    public void onStop() {
1063    }
1064
1065    @Override
1066    public void installIntentFilter() {
1067    }
1068
1069    @Override
1070    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1071    }
1072
1073
1074    @Override
1075    public boolean onKeyDown(int keyCode, KeyEvent event) {
1076        return false;
1077    }
1078
1079    @Override
1080    public boolean onKeyUp(int keyCode, KeyEvent event) {
1081        return false;
1082    }
1083
1084    @Override
1085    public void onSingleTapUp(View view, int x, int y) {
1086    }
1087
1088    @Override
1089    public void onPreviewTextureCopied() {
1090    }
1091
1092    @Override
1093    public void onCaptureTextureCopied() {
1094    }
1095
1096    @Override
1097    public boolean updateStorageHintOnResume() {
1098        return false;
1099    }
1100
1101    @Override
1102    public void onShowSwitcherPopup() {
1103    }
1104
1105    @Override
1106    public void onMediaSaveServiceConnected(MediaSaveService s) {
1107        // do nothing.
1108    }
1109}
1110