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