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