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