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