VideoCamera.java revision d5bc2383782a06ef824856ea5712ca64f138d5fc
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.camera;
18
19import com.android.camera.ui.CameraPicker;
20import com.android.camera.ui.IndicatorControlContainer;
21import com.android.camera.ui.IndicatorControlWheelContainer;
22import com.android.camera.ui.RotateImageView;
23import com.android.camera.ui.RotateLayout;
24import com.android.camera.ui.SharePopup;
25import com.android.camera.ui.ZoomControl;
26
27import android.content.ActivityNotFoundException;
28import android.content.BroadcastReceiver;
29import android.content.ContentResolver;
30import android.content.ContentValues;
31import android.content.Context;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.res.Configuration;
35import android.graphics.Bitmap;
36import android.hardware.Camera.CameraInfo;
37import android.hardware.Camera.Parameters;
38import android.hardware.Camera.PictureCallback;
39import android.hardware.Camera.Size;
40import android.location.Location;
41import android.media.CamcorderProfile;
42import android.media.CameraProfile;
43import android.media.MediaRecorder;
44import android.net.Uri;
45import android.os.Bundle;
46import android.os.Handler;
47import android.os.Message;
48import android.os.ParcelFileDescriptor;
49import android.os.SystemClock;
50import android.provider.MediaStore;
51import android.provider.MediaStore.Video;
52import android.util.Log;
53import android.view.GestureDetector;
54import android.view.Gravity;
55import android.view.KeyEvent;
56import android.view.Menu;
57import android.view.MenuItem;
58import android.view.MenuItem.OnMenuItemClickListener;
59import android.view.MotionEvent;
60import android.view.OrientationEventListener;
61import android.view.SurfaceHolder;
62import android.view.SurfaceView;
63import android.view.View;
64import android.view.Window;
65import android.view.WindowManager;
66import android.widget.ImageView;
67import android.widget.LinearLayout;
68import android.widget.TextView;
69import android.widget.Toast;
70
71import java.io.File;
72import java.io.IOException;
73import java.text.SimpleDateFormat;
74import java.util.Date;
75import java.util.Iterator;
76import java.util.List;
77
78/**
79 * The Camcorder activity.
80 */
81public class VideoCamera extends ActivityBase
82        implements CameraPreference.OnPreferenceChangedListener,
83        ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback,
84        MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener,
85        ModePicker.OnModeChangeListener, View.OnTouchListener,
86        EffectsRecorder.EffectsListener {
87
88    private static final String TAG = "videocamera";
89
90    private static final int CHECK_DISPLAY_ROTATION = 3;
91    private static final int CLEAR_SCREEN_DELAY = 4;
92    private static final int UPDATE_RECORD_TIME = 5;
93    private static final int ENABLE_SHUTTER_BUTTON = 6;
94
95    private static final int SCREEN_DELAY = 2 * 60 * 1000;
96
97    // The brightness settings used when it is set to automatic in the system.
98    // The reason why it is set to 0.7 is just because 1.0 is too bright.
99    private static final float DEFAULT_CAMERA_BRIGHTNESS = 0.7f;
100
101    private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L;
102
103    private static final boolean SWITCH_CAMERA = true;
104    private static final boolean SWITCH_VIDEO = false;
105
106    private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms
107
108    private static final int[] TIME_LAPSE_VIDEO_QUALITY = {
109            CamcorderProfile.QUALITY_TIME_LAPSE_1080P,
110            CamcorderProfile.QUALITY_TIME_LAPSE_720P,
111            CamcorderProfile.QUALITY_TIME_LAPSE_480P,
112            CamcorderProfile.QUALITY_TIME_LAPSE_CIF,
113            1007, /* TODO: replace it with QUALITY_TIME_LAPSE_QVGA if public. */
114            CamcorderProfile.QUALITY_TIME_LAPSE_QCIF};
115
116    private static final int[] VIDEO_QUALITY = {
117            CamcorderProfile.QUALITY_1080P,
118            CamcorderProfile.QUALITY_720P,
119            CamcorderProfile.QUALITY_480P,
120            CamcorderProfile.QUALITY_CIF,
121            7, /* TODO: replace it with CamcorderProfile.QUALITY_QVGA */
122            CamcorderProfile.QUALITY_QCIF};
123
124    /**
125     * An unpublished intent flag requesting to start recording straight away
126     * and return as soon as recording is stopped.
127     * TODO: consider publishing by moving into MediaStore.
128     */
129    private static final String EXTRA_QUICK_CAPTURE =
130            "android.intent.extra.quickCapture";
131
132    private boolean mSnapshotInProgress = false;
133
134    private static final String EFFECT_BG_FROM_GALLERY = "gallery";
135
136    private android.hardware.Camera mCameraDevice;
137    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
138
139    private ComboPreferences mPreferences;
140    private PreferenceGroup mPreferenceGroup;
141
142    private View mPreviewPanel;  // The container of PreviewFrameLayout.
143    private PreviewFrameLayout mPreviewFrameLayout;
144    private SurfaceHolder mSurfaceHolder = null;
145    private IndicatorControlContainer mIndicatorControlContainer;
146    private int mSurfaceWidth;
147    private int mSurfaceHeight;
148    private View mReviewControl;
149
150    private Toast mNoShareToast;
151    // An review image having same size as preview. It is displayed when
152    // recording is stopped in capture intent.
153    private ImageView mReviewImage;
154    // A popup window that contains a bigger thumbnail and a list of apps to share.
155    private SharePopup mSharePopup;
156    // The bitmap of the last captured video thumbnail and the URI of the
157    // original video.
158    private Thumbnail mThumbnail;
159    // An imageview showing showing the last captured picture thumbnail.
160    private RotateImageView mThumbnailView;
161    private ModePicker mModePicker;
162    private ShutterButton mShutterButton;
163    private TextView mRecordingTimeView;
164    private TextView mBgLearningMessage;
165    private LinearLayout mLabelsLinearLayout;
166
167    private boolean mIsVideoCaptureIntent;
168    private boolean mQuickCapture;
169
170    private boolean mOpenCameraFail = false;
171    private boolean mCameraDisabled = false;
172
173    private long mStorageSpace;
174
175    private MediaRecorder mMediaRecorder;
176    private EffectsRecorder mEffectsRecorder;
177
178    private int mEffectType = EffectsRecorder.EFFECT_NONE;
179    private Object mEffectParameter = null;
180    private String mEffectUriFromGallery = null;
181
182    private boolean mMediaRecorderRecording = false;
183    private long mRecordingStartTime;
184    private boolean mRecordingTimeCountsDown = false;
185    private RotateLayout mRecordingTimeRect;
186    private long mOnResumeTime;
187    // The video file that the hardware camera is about to record into
188    // (or is recording into.)
189    private String mVideoFilename;
190    private ParcelFileDescriptor mVideoFileDescriptor;
191
192    // The video file that has already been recorded, and that is being
193    // examined by the user.
194    private String mCurrentVideoFilename;
195    private Uri mCurrentVideoUri;
196    private ContentValues mCurrentVideoValues;
197
198    private CamcorderProfile mProfile;
199
200    // The video duration limit. 0 menas no limit.
201    private int mMaxVideoDurationInMs;
202
203    // Time Lapse parameters.
204    private boolean mCaptureTimeLapse = false;
205    // Default 0. If it is larger than 0, the camcorder is in time lapse mode.
206    private int mTimeBetweenTimeLapseFrameCaptureMs = 0;
207    private View mTimeLapseLabel;
208
209    private int mDesiredPreviewWidth;
210    private int mDesiredPreviewHeight;
211
212    boolean mPausing = false;
213    boolean mPreviewing = false; // True if preview is started.
214    // The display rotation in degrees. This is only valid when mPreviewing is
215    // true.
216    private int mDisplayRotation;
217
218    private ContentResolver mContentResolver;
219
220    private LocationManager mLocationManager;
221
222    private final Handler mHandler = new MainHandler();
223    private Parameters mParameters;
224
225    // multiple cameras support
226    private int mNumberOfCameras;
227    private int mCameraId;
228    private int mFrontCameraId;
229    private int mBackCameraId;
230
231    private GestureDetector mPopupGestureDetector;
232
233    private MyOrientationEventListener mOrientationListener;
234    // The degrees of the device rotated clockwise from its natural orientation.
235    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
236    // The orientation compensation for icons and thumbnails. Ex: if the value
237    // is 90, the UI components should be rotated 90 degrees counter-clockwise.
238    private int mOrientationCompensation = 0;
239    // The orientation compenstaion when we start recording.
240    private int mOrientationCompensationAtRecordStart;
241
242    private static final int ZOOM_STOPPED = 0;
243    private static final int ZOOM_START = 1;
244    private static final int ZOOM_STOPPING = 2;
245
246    private int mZoomState = ZOOM_STOPPED;
247    private boolean mSmoothZoomSupported = false;
248    private int mZoomValue;  // The current zoom value.
249    private int mZoomMax;
250    private int mTargetZoomValue;
251    private ZoomControl mZoomControl;
252    private final ZoomListener mZoomListener = new ZoomListener();
253
254    // This Handler is used to post message back onto the main thread of the
255    // application
256    private class MainHandler extends Handler {
257        @Override
258        public void handleMessage(Message msg) {
259            switch (msg.what) {
260
261                case ENABLE_SHUTTER_BUTTON:
262                    mShutterButton.setEnabled(true);
263                    break;
264
265                case CLEAR_SCREEN_DELAY: {
266                    getWindow().clearFlags(
267                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
268                    break;
269                }
270
271                case UPDATE_RECORD_TIME: {
272                    updateRecordingTime();
273                    break;
274                }
275
276                case CHECK_DISPLAY_ROTATION: {
277                    // Restart the preview if display rotation has changed.
278                    // Sometimes this happens when the device is held upside
279                    // down and camera app is opened. Rotation animation will
280                    // take some time and the rotation value we have got may be
281                    // wrong. Framework does not have a callback for this now.
282                    if ((Util.getDisplayRotation(VideoCamera.this) != mDisplayRotation)
283                            && !mMediaRecorderRecording) {
284                        startPreview();
285                    }
286                    if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
287                        mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
288                    }
289                    break;
290                }
291
292                default:
293                    Log.v(TAG, "Unhandled message: " + msg.what);
294                    break;
295            }
296        }
297    }
298
299    private BroadcastReceiver mReceiver = null;
300
301    private class MyBroadcastReceiver extends BroadcastReceiver {
302        @Override
303        public void onReceive(Context context, Intent intent) {
304            String action = intent.getAction();
305            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
306                updateAndShowStorageHint();
307                stopVideoRecording();
308            } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
309                updateAndShowStorageHint();
310                updateThumbnailButton();
311            } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
312                // SD card unavailable
313                // handled in ACTION_MEDIA_EJECT
314            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
315                Toast.makeText(VideoCamera.this,
316                        getResources().getString(R.string.wait), Toast.LENGTH_LONG).show();
317            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
318                updateAndShowStorageHint();
319            }
320        }
321    }
322
323    private String createName(long dateTaken) {
324        Date date = new Date(dateTaken);
325        SimpleDateFormat dateFormat = new SimpleDateFormat(
326                getString(R.string.video_file_name_format));
327
328        return dateFormat.format(date);
329    }
330
331    @Override
332    public void onCreate(Bundle icicle) {
333        super.onCreate(icicle);
334
335        Util.initializeScreenBrightness(getWindow(), getContentResolver());
336
337        mPreferences = new ComboPreferences(this);
338        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
339        mCameraId = CameraSettings.readPreferredCameraId(mPreferences);
340
341        //Testing purpose. Launch a specific camera through the intent extras.
342        int intentCameraId = Util.getCameraFacingIntentExtras(this);
343        if (intentCameraId != -1) {
344            mCameraId = intentCameraId;
345        }
346
347        mPreferences.setLocalId(this, mCameraId);
348        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
349
350        mNumberOfCameras = CameraHolder.instance().getNumberOfCameras();
351
352        /*
353         * To reduce startup time, we start the preview in another thread.
354         * We make sure the preview is started at the end of onCreate.
355         */
356        Thread startPreviewThread = new Thread(new Runnable() {
357            public void run() {
358                try {
359                    mCameraDevice = Util.openCamera(VideoCamera.this, mCameraId);
360                    readVideoPreferences();
361                    startPreview();
362                } catch (CameraHardwareException e) {
363                    mOpenCameraFail = true;
364                } catch (CameraDisabledException e) {
365                    mCameraDisabled = true;
366                }
367            }
368        });
369        startPreviewThread.start();
370
371        Util.enterLightsOutMode(getWindow());
372
373        mContentResolver = getContentResolver();
374
375        requestWindowFeature(Window.FEATURE_PROGRESS);
376        mIsVideoCaptureIntent = isVideoCaptureIntent();
377        setContentView(R.layout.video_camera);
378        if (mIsVideoCaptureIntent) {
379            findViewById(R.id.btn_cancel).setVisibility(View.VISIBLE);
380        } else {
381            initThumbnailButton();
382            mModePicker = (ModePicker) findViewById(R.id.mode_picker);
383            mModePicker.setVisibility(View.VISIBLE);
384            mModePicker.setOnModeChangeListener(this);
385        }
386
387        mPreviewPanel = findViewById(R.id.frame_layout);
388        mPreviewFrameLayout = (PreviewFrameLayout) findViewById(R.id.frame);
389        mReviewImage = (ImageView) findViewById(R.id.review_image);
390
391        // don't set mSurfaceHolder here. We have it set ONLY within
392        // surfaceCreated / surfaceDestroyed, other parts of the code
393        // assume that when it is set, the surface is also set.
394        SurfaceView preview = (SurfaceView) findViewById(R.id.camera_preview);
395        SurfaceHolder holder = preview.getHolder();
396        holder.addCallback(this);
397        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
398
399        mQuickCapture = getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
400
401        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
402        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_video);
403        mShutterButton.setOnShutterButtonListener(this);
404        mShutterButton.requestFocus();
405
406        mRecordingTimeView = (TextView) findViewById(R.id.recording_time);
407        mRecordingTimeRect = (RotateLayout) findViewById(R.id.recording_time_rect);
408        mOrientationListener = new MyOrientationEventListener(this);
409        mTimeLapseLabel = findViewById(R.id.time_lapse_label);
410        // The R.id.labels can only be found in phone layout. For tablet, the id is
411        // R.id.labels_w1024. That is, mLabelsLinearLayout should be null in tablet layout.
412        mLabelsLinearLayout = (LinearLayout) findViewById(R.id.labels);
413
414        mBgLearningMessage = (TextView) findViewById(R.id.bg_replace_message);
415
416        mLocationManager = new LocationManager(this, null);
417
418        // Make sure preview is started.
419        try {
420            startPreviewThread.join();
421            if (mOpenCameraFail) {
422                Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
423                return;
424            } else if (mCameraDisabled) {
425                Util.showErrorAndFinish(this, R.string.camera_disabled);
426                return;
427            }
428        } catch (InterruptedException ex) {
429            // ignore
430        }
431
432        showTimeLapseUI(mCaptureTimeLapse);
433        initializeVideoSnapshot();
434        resizeForPreviewAspectRatio();
435
436        mBackCameraId = CameraHolder.instance().getBackCameraId();
437        mFrontCameraId = CameraHolder.instance().getFrontCameraId();
438
439        initializeIndicatorControl();
440    }
441
442    private void loadCameraPreferences() {
443        CameraSettings settings = new CameraSettings(this, mParameters,
444                mCameraId, CameraHolder.instance().getCameraInfo());
445        // Remove the video quality preference setting when the quality is given in the intent.
446        mPreferenceGroup = filterPreferenceScreenByIntent(
447                settings.getPreferenceGroup(R.xml.video_preferences));
448    }
449
450    private boolean collapseCameraControls() {
451        if ((mIndicatorControlContainer != null)
452                && mIndicatorControlContainer.dismissSettingPopup()) {
453            return true;
454        }
455        return false;
456    }
457
458    private void enableCameraControls(boolean enable) {
459        if (mIndicatorControlContainer != null) {
460            mIndicatorControlContainer.setEnabled(enable);
461        }
462        if (mModePicker != null) mModePicker.setEnabled(enable);
463    }
464
465    private void initializeIndicatorControl() {
466        mIndicatorControlContainer =
467                (IndicatorControlContainer) findViewById(R.id.indicator_control);
468        if (mIndicatorControlContainer == null) return;
469        loadCameraPreferences();
470
471        final String[] SETTING_KEYS = {
472                    CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
473                    CameraSettings.KEY_WHITE_BALANCE,
474                    CameraSettings.KEY_VIDEO_EFFECT,
475                    CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
476                    CameraSettings.KEY_VIDEO_QUALITY};
477        final String[] OTHER_SETTING_KEYS = {
478                    CameraSettings.KEY_RECORD_LOCATION};
479
480        CameraPicker.setImageResourceId(R.drawable.ic_switch_video_facing_holo_light);
481        mIndicatorControlContainer.initialize(this, mPreferenceGroup,
482                mParameters.isZoomSupported(), SETTING_KEYS, OTHER_SETTING_KEYS);
483        mIndicatorControlContainer.setListener(this);
484        mPopupGestureDetector = new GestureDetector(this,
485                new PopupGestureListener());
486
487        if (effectsActive()) {
488            mIndicatorControlContainer.overrideSettings(
489                    CameraSettings.KEY_VIDEO_QUALITY,
490                    Integer.toString(CamcorderProfile.QUALITY_480P));
491        }
492    }
493
494    public static int roundOrientation(int orientation) {
495        return ((orientation + 45) / 90 * 90) % 360;
496    }
497
498    private class MyOrientationEventListener
499            extends OrientationEventListener {
500        public MyOrientationEventListener(Context context) {
501            super(context);
502        }
503
504        @Override
505        public void onOrientationChanged(int orientation) {
506            // We keep the last known orientation. So if the user first orient
507            // the camera then point the camera to floor or sky, we still have
508            // the correct orientation.
509            if (orientation == ORIENTATION_UNKNOWN) return;
510            mOrientation = roundOrientation(orientation);
511            // When the screen is unlocked, display rotation may change. Always
512            // calculate the up-to-date orientationCompensation.
513            int orientationCompensation = mOrientation
514                    + Util.getDisplayRotation(VideoCamera.this);
515
516            if (mOrientationCompensation != orientationCompensation) {
517                mOrientationCompensation = orientationCompensation;
518                if (effectsActive()) {
519                    CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
520                    int rotation = (info.orientation + mOrientation) % 360;
521                    mEffectsRecorder.setOrientationHint(rotation);
522                }
523                // Do not rotate the icons during recording because the video
524                // orientation is fixed after recording.
525                if (!mMediaRecorderRecording) {
526                    setOrientationIndicator(mOrientationCompensation);
527                }
528            }
529        }
530    }
531
532    private void setOrientationIndicator(int degree) {
533        if (mThumbnailView != null) mThumbnailView.setDegree(degree);
534        if (mModePicker != null) mModePicker.setDegree(degree);
535        if (mSharePopup != null) mSharePopup.setOrientation(degree);
536        if (mIndicatorControlContainer != null) mIndicatorControlContainer.setDegree(degree);
537        // We change the orientation of the linearlayout only for phone UI because when in portrait
538        // the width is not enough.
539        if (mLabelsLinearLayout != null) {
540            if (((degree / 90) & 1) == 1) {
541                mLabelsLinearLayout.setOrientation(mLabelsLinearLayout.VERTICAL);
542            } else {
543                mLabelsLinearLayout.setOrientation(mLabelsLinearLayout.HORIZONTAL);
544            }
545        }
546        mRecordingTimeRect.setOrientation(mOrientationCompensation);
547    }
548
549    private void startPlayVideoActivity() {
550        Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri);
551        try {
552            startActivity(intent);
553        } catch (ActivityNotFoundException ex) {
554            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
555        }
556    }
557
558    @OnClickAttr
559    public void onThumbnailClicked(View v) {
560        if (!mMediaRecorderRecording && mThumbnail != null) {
561            showSharePopup();
562        }
563    }
564
565    @OnClickAttr
566    public void onRetakeButtonClicked(View v) {
567        deleteCurrentVideo();
568        hideAlert();
569    }
570
571    @OnClickAttr
572    public void onPlayButtonClicked(View v) {
573        startPlayVideoActivity();
574    }
575
576    @OnClickAttr
577    public void onDoneButtonClicked(View v) {
578        doReturnToCaller(true);
579    }
580
581    @OnClickAttr
582    public void onCancelButtonClicked(View v) {
583        stopVideoRecording();
584        doReturnToCaller(false);
585    }
586
587    public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
588        // Do nothing (everything happens in onShutterButtonClick).
589    }
590
591    private void onStopVideoRecording(boolean valid) {
592        stopVideoRecording();
593        if (mIsVideoCaptureIntent) {
594            if (mQuickCapture) {
595                doReturnToCaller(valid);
596            } else {
597                showAlert();
598            }
599        } else {
600            getThumbnail();
601        }
602    }
603
604    public void onShutterButtonClick(ShutterButton button) {
605        switch (button.getId()) {
606            case R.id.shutter_button:
607                if (collapseCameraControls()) return;
608                boolean stop = mMediaRecorderRecording;
609
610                if (stop) {
611                    onStopVideoRecording(true);
612                } else {
613                    startVideoRecording();
614                }
615                mShutterButton.setEnabled(false);
616
617                // Keep the shutter button disabled when in video capture intent
618                // mode and recording is stopped. It'll be re-enabled when
619                // re-take button is clicked.
620                if (!(mIsVideoCaptureIntent && stop)) {
621                    mHandler.sendEmptyMessageDelayed(
622                            ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
623                }
624                break;
625        }
626    }
627
628    private OnScreenHint mStorageHint;
629
630    private void updateAndShowStorageHint() {
631        mStorageSpace = Storage.getAvailableSpace();
632        showStorageHint();
633    }
634
635    private void showStorageHint() {
636        String errorMessage = null;
637        if (mStorageSpace == Storage.UNAVAILABLE) {
638            errorMessage = getString(R.string.no_storage);
639        } else if (mStorageSpace == Storage.PREPARING) {
640            errorMessage = getString(R.string.preparing_sd);
641        } else if (mStorageSpace == Storage.UNKNOWN_SIZE) {
642            errorMessage = getString(R.string.access_sd_fail);
643        } else if (mStorageSpace < LOW_STORAGE_THRESHOLD) {
644            errorMessage = getString(R.string.spaceIsLow_content);
645        }
646
647        if (errorMessage != null) {
648            if (mStorageHint == null) {
649                mStorageHint = OnScreenHint.makeText(this, errorMessage);
650            } else {
651                mStorageHint.setText(errorMessage);
652            }
653            mStorageHint.show();
654        } else if (mStorageHint != null) {
655            mStorageHint.cancel();
656            mStorageHint = null;
657        }
658    }
659
660    private void readVideoPreferences() {
661        // The preference stores values from ListPreference and is thus string type for all values.
662        // We need to convert it to int manually.
663        String defaultQuality = CameraSettings.getDefaultVideoQuality(mCameraId,
664                getResources().getString(R.string.pref_video_quality_default));
665        String videoQuality =
666                mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY,
667                        defaultQuality);
668        int quality = Integer.valueOf(videoQuality);
669
670        // Set video quality.
671        Intent intent = getIntent();
672        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
673            int extraVideoQuality =
674                    intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
675            if (extraVideoQuality > 0) {
676                quality = CamcorderProfile.QUALITY_HIGH;
677            } else {  // 0 is mms.
678                quality = CamcorderProfile.QUALITY_LOW;
679            }
680        }
681
682        // Set video duration limit. The limit is read from the preference,
683        // unless it is specified in the intent.
684        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
685            int seconds =
686                    intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0);
687            mMaxVideoDurationInMs = 1000 * seconds;
688        } else {
689            mMaxVideoDurationInMs = CameraSettings.DEFAULT_VIDEO_DURATION;
690        }
691
692        // Set effect
693        mEffectType = CameraSettings.readEffectType(mPreferences);
694        if (mEffectType != EffectsRecorder.EFFECT_NONE) {
695            mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
696            // When picking from gallery, mEffectParameter should have been
697            // initialized in onActivityResult. If not, fall back to no effect
698            if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER
699                    && ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)
700                    && mEffectUriFromGallery == null) {
701                Log.w(TAG, "No URI from gallery, resetting to no effect");
702                mEffectType = EffectsRecorder.EFFECT_NONE;
703                mEffectParameter = null;
704                ComboPreferences.Editor editor = mPreferences.edit();
705                editor.putString(CameraSettings.KEY_VIDEO_EFFECT,
706                        getString(R.string.pref_video_effect_default));
707                editor.apply();
708                if (mIndicatorControlContainer != null) {
709                    mIndicatorControlContainer.overrideSettings(
710                        CameraSettings.KEY_VIDEO_QUALITY,
711                        null);
712                }
713            } else {
714                // Set quality to 480p for effects
715                quality = CamcorderProfile.QUALITY_480P;
716                // On initial startup, can get here before indicator control is
717                // enabled. In that case, UI quality override handled in
718                // initializeIndicatorControl.
719                if (mIndicatorControlContainer != null) {
720                    mIndicatorControlContainer.overrideSettings(
721                            CameraSettings.KEY_VIDEO_QUALITY,
722                            Integer.toString(CamcorderProfile.QUALITY_480P));
723                }
724            }
725        } else {
726            mEffectParameter = null;
727            if (mIndicatorControlContainer != null) {
728                mIndicatorControlContainer.overrideSettings(
729                        CameraSettings.KEY_VIDEO_QUALITY,
730                        null);
731            }
732        }
733        // Read time lapse recording interval.
734        String frameIntervalStr = mPreferences.getString(
735                CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
736                getString(R.string.pref_video_time_lapse_frame_interval_default));
737        mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr);
738
739        mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0);
740        // TODO: This should be checked instead directly +1000.
741        if (mCaptureTimeLapse) quality += 1000;
742        mProfile = CamcorderProfile.get(mCameraId, quality);
743        getDesiredPreviewSize();
744    }
745
746    private void getDesiredPreviewSize() {
747        mParameters = mCameraDevice.getParameters();
748        if (mParameters.getSupportedVideoSizes() == null || effectsActive()) {
749            mDesiredPreviewWidth = mProfile.videoFrameWidth;
750            mDesiredPreviewHeight = mProfile.videoFrameHeight;
751        } else {  // Driver supports separates outputs for preview and video.
752            List<Size> sizes = mParameters.getSupportedPreviewSizes();
753            Size preferred = mParameters.getPreferredPreviewSizeForVideo();
754            int product = preferred.width * preferred.height;
755            Iterator it = sizes.iterator();
756            // Remove the preview sizes that are not preferred.
757            while (it.hasNext()) {
758                Size size = (Size) it.next();
759                if (size.width * size.height > product) {
760                    it.remove();
761                }
762            }
763            Size optimalSize = Util.getOptimalPreviewSize(this, sizes,
764                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
765            mDesiredPreviewWidth = optimalSize.width;
766            mDesiredPreviewHeight = optimalSize.height;
767        }
768        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
769                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
770    }
771
772    private void resizeForPreviewAspectRatio() {
773        mPreviewFrameLayout.setAspectRatio(
774                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
775    }
776
777    @Override
778    protected void onResume() {
779        super.onResume();
780        mPausing = false;
781        if (mOpenCameraFail || mCameraDisabled) return;
782        mZoomValue = 0;
783
784        showVideoSnapshotUI(false);
785
786        // Start orientation listener as soon as possible because it takes
787        // some time to get first orientation.
788        mOrientationListener.enable();
789        if (!mPreviewing) {
790            try {
791                mCameraDevice = Util.openCamera(this, mCameraId);
792                readVideoPreferences();
793                resizeForPreviewAspectRatio();
794                startPreview();
795            } catch (CameraHardwareException e) {
796                Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
797                return;
798            } catch (CameraDisabledException e) {
799                Util.showErrorAndFinish(this, R.string.camera_disabled);
800                return;
801            }
802        }
803
804        // Initializing it here after the preview is started.
805        initializeZoom();
806
807        keepScreenOnAwhile();
808
809        // install an intent filter to receive SD card related events.
810        IntentFilter intentFilter =
811                new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
812        intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
813        intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
814        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
815        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
816        intentFilter.addDataScheme("file");
817        mReceiver = new MyBroadcastReceiver();
818        registerReceiver(mReceiver, intentFilter);
819        mStorageSpace = Storage.getAvailableSpace();
820
821        mHandler.postDelayed(new Runnable() {
822            public void run() {
823                showStorageHint();
824            }
825        }, 200);
826
827        // Initialize location sevice.
828        boolean recordLocation = RecordLocationPreference.get(
829                mPreferences, getContentResolver());
830        mLocationManager.recordLocation(recordLocation);
831
832        if (!mIsVideoCaptureIntent) {
833            updateThumbnailButton();  // Update the last video thumbnail.
834            mModePicker.setCurrentMode(ModePicker.MODE_VIDEO);
835        }
836
837        if (mPreviewing) {
838            mOnResumeTime = SystemClock.uptimeMillis();
839            mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
840        }
841    }
842
843    private void setPreviewDisplay(SurfaceHolder holder) {
844        try {
845            if (effectsActive()) {
846                mEffectsRecorder.setPreviewDisplay(
847                        mSurfaceHolder,
848                        mSurfaceWidth,
849                        mSurfaceHeight);
850            } else {
851                mCameraDevice.setPreviewDisplay(holder);
852            }
853        } catch (Throwable ex) {
854            closeCamera();
855            throw new RuntimeException("setPreviewDisplay failed", ex);
856        }
857    }
858
859    private void startPreview() {
860        Log.v(TAG, "startPreview");
861
862        mCameraDevice.setErrorCallback(mErrorCallback);
863        if (mPreviewing == true) {
864            mCameraDevice.stopPreview();
865            if (effectsActive() && mEffectsRecorder != null) {
866                mEffectsRecorder.release();
867            }
868            mPreviewing = false;
869        }
870
871        mDisplayRotation = Util.getDisplayRotation(this);
872        int orientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
873        mCameraDevice.setDisplayOrientation(orientation);
874        setCameraParameters();
875
876        if (!effectsActive()) {
877            setPreviewDisplay(mSurfaceHolder);
878            try {
879                mCameraDevice.startPreview();
880            } catch (Throwable ex) {
881                closeCamera();
882                throw new RuntimeException("startPreview failed", ex);
883            }
884        } else {
885            initializeEffectsPreview();
886            Log.v(TAG, "effectsStartPreview");
887            mEffectsRecorder.startPreview();
888        }
889
890        mZoomState = ZOOM_STOPPED;
891        mPreviewing = true;
892    }
893
894    private void closeCamera() {
895        Log.v(TAG, "closeCamera");
896        if (mCameraDevice == null) {
897            Log.d(TAG, "already stopped.");
898            return;
899        }
900        if (mEffectsRecorder != null) {
901            mEffectsRecorder.release();
902        }
903        mEffectType = EffectsRecorder.EFFECT_NONE;
904        CameraHolder.instance().release();
905        mCameraDevice.setZoomChangeListener(null);
906        mCameraDevice.setErrorCallback(null);
907        mCameraDevice = null;
908        mPreviewing = false;
909        mSnapshotInProgress = false;
910    }
911
912    private void finishRecorderAndCloseCamera() {
913        // This is similar to what mShutterButton.performClick() does,
914        // but not quite the same.
915        if (mMediaRecorderRecording) {
916            if (mIsVideoCaptureIntent) {
917                stopVideoRecording();
918                showAlert();
919            } else {
920                stopVideoRecording();
921                getThumbnail();
922            }
923        } else {
924            stopVideoRecording();
925        }
926        closeCamera();
927    }
928
929    @Override
930    protected void onPause() {
931        super.onPause();
932        mPausing = true;
933
934        if (mIndicatorControlContainer != null) {
935            mIndicatorControlContainer.dismissSettingPopup();
936        }
937
938        finishRecorderAndCloseCamera();
939
940        if (mSharePopup != null) mSharePopup.dismiss();
941
942        if (mReceiver != null) {
943            unregisterReceiver(mReceiver);
944            mReceiver = null;
945        }
946        resetScreenOn();
947
948        if (!mIsVideoCaptureIntent && mThumbnail != null && !mThumbnail.fromFile()) {
949            mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
950        }
951
952        if (mStorageHint != null) {
953            mStorageHint.cancel();
954            mStorageHint = null;
955        }
956
957        mOrientationListener.disable();
958        mLocationManager.recordLocation(false);
959
960        mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
961    }
962
963    @Override
964    public void onUserInteraction() {
965        super.onUserInteraction();
966        if (!mMediaRecorderRecording) keepScreenOnAwhile();
967    }
968
969    @Override
970    public void onBackPressed() {
971        if (mPausing) return;
972        if (mMediaRecorderRecording) {
973            onStopVideoRecording(false);
974        } else if (!collapseCameraControls()) {
975            super.onBackPressed();
976        }
977    }
978
979    @Override
980    public boolean onKeyDown(int keyCode, KeyEvent event) {
981        // Do not handle any key if the activity is paused.
982        if (mPausing) {
983            return true;
984        }
985
986        switch (keyCode) {
987            case KeyEvent.KEYCODE_CAMERA:
988                if (event.getRepeatCount() == 0) {
989                    mShutterButton.performClick();
990                    return true;
991                }
992                break;
993            case KeyEvent.KEYCODE_DPAD_CENTER:
994                if (event.getRepeatCount() == 0) {
995                    mShutterButton.performClick();
996                    return true;
997                }
998                break;
999            case KeyEvent.KEYCODE_MENU:
1000                if (mMediaRecorderRecording) return true;
1001                break;
1002        }
1003
1004        return super.onKeyDown(keyCode, event);
1005    }
1006
1007    @Override
1008    public boolean onKeyUp(int keyCode, KeyEvent event) {
1009        switch (keyCode) {
1010            case KeyEvent.KEYCODE_CAMERA:
1011                mShutterButton.setPressed(false);
1012                return true;
1013        }
1014        return super.onKeyUp(keyCode, event);
1015    }
1016
1017    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
1018        // Make sure we have a surface in the holder before proceeding.
1019        if (holder.getSurface() == null) {
1020            Log.d(TAG, "holder.getSurface() == null");
1021            return;
1022        }
1023
1024        Log.v(TAG, "surfaceChanged. w=" + w + ". h=" + h);
1025
1026        mSurfaceHolder = holder;
1027        mSurfaceWidth = w;
1028        mSurfaceHeight = h;
1029
1030        if (mPausing) {
1031            // We're pausing, the screen is off and we already stopped
1032            // video recording. We don't want to start the camera again
1033            // in this case in order to conserve power.
1034            // The fact that surfaceChanged is called _after_ an onPause appears
1035            // to be legitimate since in that case the lockscreen always returns
1036            // to portrait orientation possibly triggering the notification.
1037            return;
1038        }
1039
1040        // The mCameraDevice will be null if it is fail to connect to the
1041        // camera hardware. In this case we will show a dialog and then
1042        // finish the activity, so it's OK to ignore it.
1043        if (mCameraDevice == null) return;
1044
1045        // Set preview display if the surface is being created. Preview was
1046        // already started. Also restart the preview if display rotation has
1047        // changed. Sometimes this happens when the device is held in portrait
1048        // and camera app is opened. Rotation animation takes some time and
1049        // display rotation in onCreate may not be what we want.
1050        if (mPreviewing && (Util.getDisplayRotation(this) == mDisplayRotation)
1051                && holder.isCreating()) {
1052            setPreviewDisplay(holder);
1053        } else {
1054            stopVideoRecording();
1055            startPreview();
1056        }
1057    }
1058
1059    public void surfaceCreated(SurfaceHolder holder) {
1060    }
1061
1062    public void surfaceDestroyed(SurfaceHolder holder) {
1063        mSurfaceHolder = null;
1064    }
1065
1066    private void gotoGallery() {
1067        MenuHelper.gotoCameraVideoGallery(this);
1068    }
1069
1070    @Override
1071    public boolean onCreateOptionsMenu(Menu menu) {
1072        super.onCreateOptionsMenu(menu);
1073
1074        if (mIsVideoCaptureIntent) {
1075            // No options menu for attach mode.
1076            return false;
1077        } else {
1078            addBaseMenuItems(menu);
1079        }
1080        return true;
1081    }
1082
1083    private boolean isVideoCaptureIntent() {
1084        String action = getIntent().getAction();
1085        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
1086    }
1087
1088    private void doReturnToCaller(boolean valid) {
1089        Intent resultIntent = new Intent();
1090        int resultCode;
1091        if (valid) {
1092            resultCode = RESULT_OK;
1093            resultIntent.setData(mCurrentVideoUri);
1094        } else {
1095            resultCode = RESULT_CANCELED;
1096        }
1097        setResultEx(resultCode, resultIntent);
1098        finish();
1099    }
1100
1101    private void cleanupEmptyFile() {
1102        if (mVideoFilename != null) {
1103            File f = new File(mVideoFilename);
1104            if (f.length() == 0 && f.delete()) {
1105                Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
1106                mVideoFilename = null;
1107            }
1108        }
1109    }
1110
1111    // Prepares media recorder.
1112    private void initializeRecorder() {
1113        Log.v(TAG, "initializeRecorder");
1114        // If the mCameraDevice is null, then this activity is going to finish
1115        if (mCameraDevice == null) return;
1116
1117        if (mSurfaceHolder == null) {
1118            Log.v(TAG, "Surface holder is null. Wait for surface changed.");
1119            return;
1120        }
1121
1122        Intent intent = getIntent();
1123        Bundle myExtras = intent.getExtras();
1124
1125        long requestedSizeLimit = 0;
1126        if (mIsVideoCaptureIntent && myExtras != null) {
1127            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
1128            if (saveUri != null) {
1129                try {
1130                    mVideoFileDescriptor =
1131                            mContentResolver.openFileDescriptor(saveUri, "rw");
1132                    mCurrentVideoUri = saveUri;
1133                } catch (java.io.FileNotFoundException ex) {
1134                    // invalid uri
1135                    Log.e(TAG, ex.toString());
1136                }
1137            }
1138            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
1139        }
1140        mMediaRecorder = new MediaRecorder();
1141
1142        // Unlock the camera object before passing it to media recorder.
1143        mCameraDevice.unlock();
1144        mMediaRecorder.setCamera(mCameraDevice);
1145        if (!mCaptureTimeLapse) {
1146            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
1147        }
1148        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
1149        mMediaRecorder.setProfile(mProfile);
1150        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
1151        if (mCaptureTimeLapse) {
1152            mMediaRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
1153        }
1154
1155        Location loc = mLocationManager.getCurrentLocation();
1156        if (loc != null) {
1157            mMediaRecorder.setLocation((float) loc.getLatitude(),
1158                    (float) loc.getLongitude());
1159        }
1160
1161
1162        // Set output file.
1163        // Try Uri in the intent first. If it doesn't exist, use our own
1164        // instead.
1165        if (mVideoFileDescriptor != null) {
1166            mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
1167        } else {
1168            generateVideoFilename(mProfile.fileFormat);
1169            mMediaRecorder.setOutputFile(mVideoFilename);
1170        }
1171
1172        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
1173
1174        // Set maximum file size.
1175        // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter
1176        // of that to make it more likely that recording can complete
1177        // successfully.
1178        long maxFileSize = mStorageSpace - LOW_STORAGE_THRESHOLD / 4;
1179        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
1180            maxFileSize = requestedSizeLimit;
1181        }
1182
1183        try {
1184            mMediaRecorder.setMaxFileSize(maxFileSize);
1185        } catch (RuntimeException exception) {
1186            // We are going to ignore failure of setMaxFileSize here, as
1187            // a) The composer selected may simply not support it, or
1188            // b) The underlying media framework may not handle 64-bit range
1189            // on the size restriction.
1190        }
1191
1192        // See android.hardware.Camera.Parameters.setRotation for
1193        // documentation.
1194        // Note that mOrientation here is the device orientation, which is the opposite of
1195        // what activity.getWindowManager().getDefaultDisplay().getRotation() would return,
1196        // which is the orientation the graphics need to rotate in order to render correctly.
1197        int rotation = 0;
1198        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
1199            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
1200            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
1201                rotation = (info.orientation - mOrientation + 360) % 360;
1202            } else {  // back-facing camera
1203                rotation = (info.orientation + mOrientation) % 360;
1204            }
1205        }
1206        mMediaRecorder.setOrientationHint(rotation);
1207        mOrientationCompensationAtRecordStart = mOrientationCompensation;
1208
1209        try {
1210            mMediaRecorder.prepare();
1211        } catch (IOException e) {
1212            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
1213            releaseMediaRecorder();
1214            throw new RuntimeException(e);
1215        }
1216
1217        mMediaRecorder.setOnErrorListener(this);
1218        mMediaRecorder.setOnInfoListener(this);
1219    }
1220
1221    private void initializeEffectsPreview() {
1222        Log.v(TAG, "initializeEffectsPreview");
1223        // If the mCameraDevice is null, then this activity is going to finish
1224        if (mCameraDevice == null) return;
1225
1226        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
1227
1228        mEffectsRecorder = new EffectsRecorder(this);
1229
1230        mEffectsRecorder.setCamera(mCameraDevice);
1231        mEffectsRecorder.setCameraFacing(info.facing);
1232        mEffectsRecorder.setProfile(mProfile);
1233        mEffectsRecorder.setEffectsListener(this);
1234        mEffectsRecorder.setOnInfoListener(this);
1235        mEffectsRecorder.setOnErrorListener(this);
1236
1237        // See android.hardware.Camera.Parameters.setRotation for
1238        // documentation.
1239        int rotation = 0;
1240        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
1241            rotation = (info.orientation + mOrientation) % 360;
1242        }
1243        mEffectsRecorder.setOrientationHint(rotation);
1244        mOrientationCompensationAtRecordStart = mOrientationCompensation;
1245
1246        mEffectsRecorder.setPreviewDisplay(
1247                mSurfaceHolder,
1248                mSurfaceWidth,
1249                mSurfaceHeight);
1250
1251        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
1252            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
1253            mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery);
1254        } else {
1255            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
1256        }
1257    }
1258
1259    private void initializeEffectsRecording() {
1260        Log.v(TAG, "initializeEffectsRecording");
1261
1262        Intent intent = getIntent();
1263        Bundle myExtras = intent.getExtras();
1264
1265        if (mIsVideoCaptureIntent && myExtras != null) {
1266            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
1267            if (saveUri != null) {
1268                mVideoFilename = saveUri.toString();
1269            } else {
1270                mVideoFilename = null;
1271            }
1272        } else {
1273            mVideoFilename = null;
1274        }
1275
1276        // TODO: Timelapse
1277
1278        // Set output file
1279        if (mVideoFilename == null) {
1280            generateVideoFilename(mProfile.fileFormat);
1281        }
1282        mEffectsRecorder.setOutputFile(mVideoFilename);
1283    }
1284
1285
1286    private void releaseMediaRecorder() {
1287        Log.v(TAG, "Releasing media recorder.");
1288        if (mMediaRecorder != null) {
1289            cleanupEmptyFile();
1290            mMediaRecorder.reset();
1291            mMediaRecorder.release();
1292            mMediaRecorder = null;
1293        }
1294        mVideoFilename = null;
1295        if (mVideoFileDescriptor != null) {
1296            try {
1297                mVideoFileDescriptor.close();
1298            } catch (IOException e) {
1299                Log.e(TAG, "Fail to close fd", e);
1300            }
1301            mVideoFileDescriptor = null;
1302        }
1303    }
1304
1305    private void releaseEffectsRecorder() {
1306        Log.v(TAG, "Releasing effects recorder.");
1307        if (mEffectsRecorder != null) {
1308            cleanupEmptyFile();
1309            mEffectsRecorder.release();
1310            mEffectsRecorder = null;
1311        }
1312        mVideoFilename = null;
1313    }
1314
1315    private void generateVideoFilename(int outputFileFormat) {
1316        long dateTaken = System.currentTimeMillis();
1317        String title = createName(dateTaken);
1318        String filename = title + ".3gp"; // Used when emailing.
1319        String mime = "video/3gpp";
1320        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
1321            filename = title + ".mp4";
1322            mime = "video/mp4";
1323        }
1324        mVideoFilename = Storage.DIRECTORY + '/' + filename;
1325        mCurrentVideoValues = new ContentValues(7);
1326        mCurrentVideoValues.put(Video.Media.TITLE, title);
1327        mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename);
1328        mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken);
1329        mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime);
1330        mCurrentVideoValues.put(Video.Media.DATA, mVideoFilename);
1331        mCurrentVideoValues.put(Video.Media.RESOLUTION,
1332                Integer.toString(mProfile.videoFrameWidth) + "x" +
1333                Integer.toString(mProfile.videoFrameHeight));
1334        Log.v(TAG, "New video filename: " + mVideoFilename);
1335    }
1336
1337    private void addVideoToMediaStore() {
1338        if (mVideoFileDescriptor == null) {
1339            Uri videoTable = Uri.parse("content://media/external/video/media");
1340            mCurrentVideoValues.put(Video.Media.SIZE,
1341                    new File(mCurrentVideoFilename).length());
1342            long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
1343            if (duration > 0) {
1344                if (mCaptureTimeLapse) {
1345                    duration = getTimeLapseVideoLength(duration);
1346                }
1347                mCurrentVideoValues.put(Video.Media.DURATION, duration);
1348            } else {
1349                Log.w(TAG, "Video duration <= 0 : " + duration);
1350            }
1351            try {
1352                mCurrentVideoUri = mContentResolver.insert(videoTable,
1353                        mCurrentVideoValues);
1354                sendBroadcast(new Intent(android.hardware.Camera.ACTION_NEW_VIDEO,
1355                        mCurrentVideoUri));
1356            } catch (Exception e) {
1357                // We failed to insert into the database. This can happen if
1358                // the SD card is unmounted.
1359                mCurrentVideoUri = null;
1360                mCurrentVideoFilename = null;
1361            } finally {
1362                Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
1363            }
1364        }
1365        mCurrentVideoValues = null;
1366    }
1367
1368    private void deleteCurrentVideo() {
1369        // Remove the video and the uri if the uri is not passed in by intent.
1370        if (mCurrentVideoFilename != null) {
1371            deleteVideoFile(mCurrentVideoFilename);
1372            mCurrentVideoFilename = null;
1373            if (mCurrentVideoUri != null) {
1374                mContentResolver.delete(mCurrentVideoUri, null, null);
1375                mCurrentVideoUri = null;
1376            }
1377        }
1378        updateAndShowStorageHint();
1379    }
1380
1381    private void deleteVideoFile(String fileName) {
1382        Log.v(TAG, "Deleting video " + fileName);
1383        File f = new File(fileName);
1384        if (!f.delete()) {
1385            Log.v(TAG, "Could not delete " + fileName);
1386        }
1387    }
1388
1389    private void addBaseMenuItems(Menu menu) {
1390        MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() {
1391            public void run() {
1392                switchToOtherMode(ModePicker.MODE_CAMERA);
1393            }
1394        });
1395        MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_PANORAMA, new Runnable() {
1396            public void run() {
1397                switchToOtherMode(ModePicker.MODE_PANORAMA);
1398            }
1399        });
1400
1401        if (mNumberOfCameras > 1) {
1402            menu.add(R.string.switch_camera_id)
1403                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
1404                public boolean onMenuItemClick(MenuItem item) {
1405                    CameraSettings.writePreferredCameraId(mPreferences,
1406                            ((mCameraId == mFrontCameraId)
1407                            ? mBackCameraId : mFrontCameraId));
1408                    onSharedPreferenceChanged();
1409                    return true;
1410                }
1411            }).setIcon(android.R.drawable.ic_menu_camera);
1412        }
1413    }
1414
1415    private PreferenceGroup filterPreferenceScreenByIntent(
1416            PreferenceGroup screen) {
1417        Intent intent = getIntent();
1418        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
1419            CameraSettings.removePreferenceFromScreen(screen,
1420                    CameraSettings.KEY_VIDEO_QUALITY);
1421        }
1422
1423        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
1424            CameraSettings.removePreferenceFromScreen(screen,
1425                    CameraSettings.KEY_VIDEO_QUALITY);
1426        }
1427        return screen;
1428    }
1429
1430    // from MediaRecorder.OnErrorListener
1431    public void onError(MediaRecorder mr, int what, int extra) {
1432        Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra);
1433        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
1434            // We may have run out of space on the sdcard.
1435            stopVideoRecording();
1436            updateAndShowStorageHint();
1437        }
1438    }
1439
1440    // from MediaRecorder.OnInfoListener
1441    public void onInfo(MediaRecorder mr, int what, int extra) {
1442        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
1443            if (mMediaRecorderRecording) onStopVideoRecording(true);
1444        } else if (what
1445                == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
1446            if (mMediaRecorderRecording) onStopVideoRecording(true);
1447
1448            // Show the toast.
1449            Toast.makeText(this, R.string.video_reach_size_limit,
1450                           Toast.LENGTH_LONG).show();
1451        }
1452    }
1453
1454    /*
1455     * Make sure we're not recording music playing in the background, ask the
1456     * MediaPlaybackService to pause playback.
1457     */
1458    private void pauseAudioPlayback() {
1459        // Shamelessly copied from MediaPlaybackService.java, which
1460        // should be public, but isn't.
1461        Intent i = new Intent("com.android.music.musicservicecommand");
1462        i.putExtra("command", "pause");
1463
1464        sendBroadcast(i);
1465    }
1466
1467    // For testing.
1468    public boolean isRecording() {
1469        return mMediaRecorderRecording;
1470    }
1471
1472    private void startVideoRecording() {
1473        Log.v(TAG, "startVideoRecording");
1474
1475        updateAndShowStorageHint();
1476        if (mStorageSpace < LOW_STORAGE_THRESHOLD) {
1477            Log.v(TAG, "Storage issue, ignore the start request");
1478            return;
1479        }
1480
1481        if (effectsActive()) {
1482            initializeEffectsRecording();
1483            if (mEffectsRecorder == null) {
1484                Log.e(TAG, "Fail to initialize effect recorder");
1485                return;
1486            }
1487        } else {
1488            initializeRecorder();
1489            if (mMediaRecorder == null) {
1490                Log.e(TAG, "Fail to initialize media recorder");
1491                return;
1492            }
1493        }
1494
1495        pauseAudioPlayback();
1496
1497        if (effectsActive()) {
1498            try {
1499                mEffectsRecorder.startRecording();
1500            } catch (RuntimeException e) {
1501                Log.e(TAG, "Could not start effects recorder. ", e);
1502                releaseEffectsRecorder();
1503                return;
1504            }
1505        } else {
1506            try {
1507                mMediaRecorder.start(); // Recording is now started
1508            } catch (RuntimeException e) {
1509                Log.e(TAG, "Could not start media recorder. ", e);
1510                releaseMediaRecorder();
1511                // If start fails, frameworks will not lock the camera for us.
1512                mCameraDevice.lock();
1513                return;
1514            }
1515        }
1516
1517        enableCameraControls(false);
1518
1519        mMediaRecorderRecording = true;
1520        mRecordingStartTime = SystemClock.uptimeMillis();
1521        showRecordingUI(true);
1522
1523        updateRecordingTime();
1524        keepScreenOn();
1525    }
1526
1527    private void showRecordingUI(boolean recording) {
1528        if (recording) {
1529            mIndicatorControlContainer.dismissSecondLevelIndicator();
1530            if (mThumbnailView != null) mThumbnailView.setEnabled(false);
1531            mShutterButton.setBackgroundResource(R.drawable.btn_shutter_video_recording);
1532            mRecordingTimeView.setText("");
1533            mRecordingTimeView.setVisibility(View.VISIBLE);
1534            if (mReviewControl != null) mReviewControl.setVisibility(View.GONE);
1535            if (mCaptureTimeLapse) {
1536                if (Util.isTabletUI()) {
1537                    ((IndicatorControlWheelContainer) mIndicatorControlContainer)
1538                            .startTimeLapseAnimation(
1539                                    mTimeBetweenTimeLapseFrameCaptureMs,
1540                                    mRecordingStartTime);
1541                }
1542            }
1543        } else {
1544            if (mThumbnailView != null) mThumbnailView.setEnabled(true);
1545            mShutterButton.setBackgroundResource(R.drawable.btn_shutter_video);
1546            mRecordingTimeView.setVisibility(View.GONE);
1547            if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE);
1548            if (mCaptureTimeLapse) {
1549                if (Util.isTabletUI()) {
1550                    ((IndicatorControlWheelContainer) mIndicatorControlContainer)
1551                            .stopTimeLapseAnimation();
1552                }
1553            }
1554        }
1555    }
1556
1557    private void getThumbnail() {
1558        if (mCurrentVideoUri != null) {
1559            Bitmap videoFrame = Thumbnail.createVideoThumbnail(mCurrentVideoFilename,
1560                    mPreviewFrameLayout.getWidth());
1561            if (videoFrame != null) {
1562                mThumbnail = new Thumbnail(mCurrentVideoUri, videoFrame, 0);
1563                mThumbnailView.setBitmap(mThumbnail.getBitmap());
1564                // Share popup may still have the reference to the old thumbnail. Clear it.
1565                mSharePopup = null;
1566            }
1567        }
1568    }
1569
1570    private void showAlert() {
1571        if (mCurrentVideoFilename != null) {
1572            Bitmap bitmap = Thumbnail.createVideoThumbnail(mCurrentVideoFilename,
1573                    mPreviewFrameLayout.getWidth());
1574            if (bitmap != null) {
1575                // MetadataRetriever already rotates the thumbnail. We should rotate
1576                // it to match the UI orientation (and mirror if it is front-facing camera).
1577                CameraInfo[] info = CameraHolder.instance().getCameraInfo();
1578                boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
1579                bitmap = Util.rotateAndMirror(bitmap, -mOrientationCompensationAtRecordStart,
1580                        mirror);
1581                mReviewImage.setImageBitmap(bitmap);
1582                mReviewImage.setVisibility(View.VISIBLE);
1583            }
1584        }
1585
1586        Util.fadeOut(mShutterButton);
1587        Util.fadeOut(mIndicatorControlContainer);
1588        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1589        for (int id : pickIds) {
1590            Util.fadeIn(findViewById(id));
1591        }
1592    }
1593
1594    private void hideAlert() {
1595        mReviewImage.setVisibility(View.GONE);
1596        mShutterButton.setEnabled(true);
1597        enableCameraControls(true);
1598
1599        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1600        for (int id : pickIds) {
1601            Util.fadeOut(findViewById(id));
1602        }
1603        Util.fadeIn(mShutterButton);
1604        Util.fadeIn(mIndicatorControlContainer);
1605
1606        if (mCaptureTimeLapse) {
1607            showTimeLapseUI(true);
1608        }
1609    }
1610
1611    private void stopVideoRecording() {
1612        Log.v(TAG, "stopVideoRecording");
1613        if (mMediaRecorderRecording) {
1614            boolean shouldAddToMediaStore = false;
1615
1616            try {
1617                if (effectsActive()) {
1618                    mEffectsRecorder.stopRecording();
1619                } else {
1620                    mMediaRecorder.setOnErrorListener(null);
1621                    mMediaRecorder.setOnInfoListener(null);
1622                    mMediaRecorder.stop();
1623                }
1624                mCurrentVideoFilename = mVideoFilename;
1625                Log.v(TAG, "Setting current video filename: "
1626                        + mCurrentVideoFilename);
1627                shouldAddToMediaStore = true;
1628            } catch (RuntimeException e) {
1629                Log.e(TAG, "stop fail",  e);
1630                if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
1631            }
1632
1633            mMediaRecorderRecording = false;
1634            showRecordingUI(false);
1635            if (!mIsVideoCaptureIntent) {
1636                enableCameraControls(true);
1637            }
1638            keepScreenOnAwhile();
1639            if (shouldAddToMediaStore && mStorageSpace >= LOW_STORAGE_THRESHOLD) {
1640                addVideoToMediaStore();
1641            }
1642        }
1643        // always release media recorder
1644        if (!effectsActive()) {
1645            releaseMediaRecorder();
1646        }
1647    }
1648
1649    private void resetScreenOn() {
1650        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1651        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1652    }
1653
1654    private void keepScreenOnAwhile() {
1655        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1656        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1657        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1658    }
1659
1660    private void keepScreenOn() {
1661        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1662        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1663    }
1664
1665    private void initThumbnailButton() {
1666        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
1667        mThumbnailView.enableFilter(false);
1668        mThumbnailView.setVisibility(View.VISIBLE);
1669        // Load the thumbnail from the disk.
1670        mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
1671    }
1672
1673    private void updateThumbnailButton() {
1674        if (mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), mContentResolver)) {
1675            mThumbnail = Thumbnail.getLastThumbnail(mContentResolver);
1676        }
1677        if (mThumbnail != null) {
1678            mThumbnailView.setBitmap(mThumbnail.getBitmap());
1679        } else {
1680            mThumbnailView.setBitmap(null);
1681        }
1682    }
1683
1684    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
1685        long seconds = milliSeconds / 1000; // round down to compute seconds
1686        long minutes = seconds / 60;
1687        long hours = minutes / 60;
1688        long remainderMinutes = minutes - (hours * 60);
1689        long remainderSeconds = seconds - (minutes * 60);
1690
1691        StringBuilder timeStringBuilder = new StringBuilder();
1692
1693        // Hours
1694        if (hours > 0) {
1695            if (hours < 10) {
1696                timeStringBuilder.append('0');
1697            }
1698            timeStringBuilder.append(hours);
1699
1700            timeStringBuilder.append(':');
1701        }
1702
1703        // Minutes
1704        if (remainderMinutes < 10) {
1705            timeStringBuilder.append('0');
1706        }
1707        timeStringBuilder.append(remainderMinutes);
1708        timeStringBuilder.append(':');
1709
1710        // Seconds
1711        if (remainderSeconds < 10) {
1712            timeStringBuilder.append('0');
1713        }
1714        timeStringBuilder.append(remainderSeconds);
1715
1716        // Centi seconds
1717        if (displayCentiSeconds) {
1718            timeStringBuilder.append('.');
1719            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
1720            if (remainderCentiSeconds < 10) {
1721                timeStringBuilder.append('0');
1722            }
1723            timeStringBuilder.append(remainderCentiSeconds);
1724        }
1725
1726        return timeStringBuilder.toString();
1727    }
1728
1729    private long getTimeLapseVideoLength(long deltaMs) {
1730        // For better approximation calculate fractional number of frames captured.
1731        // This will update the video time at a higher resolution.
1732        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
1733        return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
1734    }
1735
1736    private void updateRecordingTime() {
1737        if (!mMediaRecorderRecording) {
1738            return;
1739        }
1740        long now = SystemClock.uptimeMillis();
1741        long delta = now - mRecordingStartTime;
1742
1743        // Starting a minute before reaching the max duration
1744        // limit, we'll countdown the remaining time instead.
1745        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
1746                && delta >= mMaxVideoDurationInMs - 60000);
1747
1748        long deltaAdjusted = delta;
1749        if (countdownRemainingTime) {
1750            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
1751        }
1752        String text;
1753
1754        long targetNextUpdateDelay;
1755        if (!mCaptureTimeLapse) {
1756            text = millisecondToTimeString(deltaAdjusted, false);
1757            targetNextUpdateDelay = 1000;
1758        } else {
1759            // The length of time lapse video is different from the length
1760            // of the actual wall clock time elapsed. Display the video length
1761            // only in format hh:mm:ss.dd, where dd are the centi seconds.
1762            text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
1763            targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
1764        }
1765
1766        mRecordingTimeView.setText(text);
1767
1768        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1769            // Avoid setting the color on every update, do it only
1770            // when it needs changing.
1771            mRecordingTimeCountsDown = countdownRemainingTime;
1772
1773            int color = getResources().getColor(countdownRemainingTime
1774                    ? R.color.recording_time_remaining_text
1775                    : R.color.recording_time_elapsed_text);
1776
1777            mRecordingTimeView.setTextColor(color);
1778        }
1779
1780        long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
1781        mHandler.sendEmptyMessageDelayed(
1782                UPDATE_RECORD_TIME, actualNextUpdateDelay);
1783    }
1784
1785    private static boolean isSupported(String value, List<String> supported) {
1786        return supported == null ? false : supported.indexOf(value) >= 0;
1787    }
1788
1789    private void setCameraParameters() {
1790        mParameters = mCameraDevice.getParameters();
1791
1792        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
1793        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
1794
1795        // Set flash mode.
1796        String flashMode = mPreferences.getString(
1797                CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
1798                getString(R.string.pref_camera_video_flashmode_default));
1799        List<String> supportedFlash = mParameters.getSupportedFlashModes();
1800        if (isSupported(flashMode, supportedFlash)) {
1801            mParameters.setFlashMode(flashMode);
1802        } else {
1803            flashMode = mParameters.getFlashMode();
1804            if (flashMode == null) {
1805                flashMode = getString(
1806                        R.string.pref_camera_flashmode_no_flash);
1807            }
1808        }
1809
1810        // Set white balance parameter.
1811        String whiteBalance = mPreferences.getString(
1812                CameraSettings.KEY_WHITE_BALANCE,
1813                getString(R.string.pref_camera_whitebalance_default));
1814        if (isSupported(whiteBalance,
1815                mParameters.getSupportedWhiteBalance())) {
1816            mParameters.setWhiteBalance(whiteBalance);
1817        } else {
1818            whiteBalance = mParameters.getWhiteBalance();
1819            if (whiteBalance == null) {
1820                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
1821            }
1822        }
1823
1824        // Set zoom.
1825        if (mParameters.isZoomSupported()) {
1826            mParameters.setZoom(mZoomValue);
1827        }
1828
1829        // Set continuous autofocus.
1830        List<String> supportedFocus = mParameters.getSupportedFocusModes();
1831        if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
1832            mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
1833        }
1834
1835        mParameters.setRecordingHint(true);
1836
1837        // Set picture size.
1838        String pictureSize = mPreferences.getString(
1839                CameraSettings.KEY_PICTURE_SIZE, null);
1840        if (pictureSize == null) {
1841            CameraSettings.initialCameraPictureSize(this, mParameters);
1842        } else {
1843            List<Size> supported = mParameters.getSupportedPictureSizes();
1844            CameraSettings.setCameraPictureSize(
1845                    pictureSize, supported, mParameters);
1846        }
1847
1848        // Set JPEG quality.
1849        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
1850                CameraProfile.QUALITY_HIGH);
1851        mParameters.setJpegQuality(jpegQuality);
1852
1853        mCameraDevice.setParameters(mParameters);
1854        // Keep preview size up to date.
1855        mParameters = mCameraDevice.getParameters();
1856    }
1857
1858    private boolean switchToOtherMode(int mode) {
1859        if (isFinishing()) return false;
1860        MenuHelper.gotoMode(mode, this);
1861        finish();
1862        return true;
1863    }
1864
1865    public boolean onModeChanged(int mode) {
1866        if (mode != ModePicker.MODE_VIDEO) {
1867            return switchToOtherMode(mode);
1868        } else {
1869            return true;
1870        }
1871    }
1872
1873    @Override
1874    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1875        switch (requestCode) {
1876            case EffectsRecorder.EFFECT_BACKDROPPER:
1877                if (resultCode == RESULT_OK) {
1878                    // onActivityResult() runs before onResume(), so this parameter will be
1879                    // seen by startPreview from onResume()
1880                    mEffectUriFromGallery = ((Uri) data.getData()).toString();
1881                    Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
1882                }
1883                break;
1884            default:
1885                Log.e(TAG, "Unknown activity result sent to Camera!");
1886                break;
1887        }
1888    }
1889
1890    @Override
1891    public void onEffectsUpdate(int effectId, int effectMsg) {
1892        if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
1893            // Effects have shut down. Hide learning message if any,
1894            // and restart regular preview.
1895            mBgLearningMessage.setVisibility(View.GONE);
1896            checkQualityAndStartPreview();
1897        } else if (effectId == EffectsRecorder.EFFECT_BACKDROPPER) {
1898            switch (effectMsg) {
1899                case EffectsRecorder.EFFECT_MSG_STARTED_LEARNING:
1900                    mBgLearningMessage.setVisibility(View.VISIBLE);
1901                    break;
1902                case EffectsRecorder.EFFECT_MSG_DONE_LEARNING:
1903                case EffectsRecorder.EFFECT_MSG_SWITCHING_EFFECT:
1904                    mBgLearningMessage.setVisibility(View.GONE);
1905                    break;
1906            }
1907        }
1908    }
1909
1910    @Override
1911    public synchronized void onEffectsError(Exception exception, String fileName) {
1912        // TODO: Eventually we may want to show the user an error dialog, and then restart the
1913        // camera and encoder gracefully. For now, we just delete the file and bail out.
1914        if (fileName != null && new File(fileName).exists()) {
1915            deleteVideoFile(fileName);
1916        }
1917        throw new RuntimeException("Error during recording!", exception);
1918    }
1919
1920    @Override
1921    public void onConfigurationChanged(Configuration config) {
1922        super.onConfigurationChanged(config);
1923    }
1924
1925    public void onOverriddenPreferencesClicked() {
1926    }
1927
1928    public void onRestorePreferencesClicked() {
1929        Runnable runnable = new Runnable() {
1930            public void run() {
1931                restorePreferences();
1932            }
1933        };
1934        MenuHelper.confirmAction(this,
1935                getString(R.string.confirm_restore_title),
1936                getString(R.string.confirm_restore_message),
1937                runnable);
1938    }
1939
1940    private void restorePreferences() {
1941        // Reset the zoom. Zoom value is not stored in preference.
1942        if (mParameters.isZoomSupported()) {
1943            mZoomValue = 0;
1944            setCameraParameters();
1945            mZoomControl.setZoomIndex(0);
1946        }
1947
1948        if (mIndicatorControlContainer != null) {
1949            mIndicatorControlContainer.dismissSettingPopup();
1950            CameraSettings.restorePreferences(this, mPreferences,
1951                    mParameters);
1952            mIndicatorControlContainer.reloadPreferences();
1953            onSharedPreferenceChanged();
1954        }
1955    }
1956
1957    private boolean effectsActive() {
1958        return (mEffectType != EffectsRecorder.EFFECT_NONE);
1959    }
1960
1961    public void onSharedPreferenceChanged() {
1962        // ignore the events after "onPause()" or preview has not started yet
1963        if (mPausing) return;
1964        synchronized (mPreferences) {
1965            // If mCameraDevice is not ready then we can set the parameter in
1966            // startPreview().
1967            if (mCameraDevice == null) return;
1968
1969            boolean recordLocation = RecordLocationPreference.get(
1970                    mPreferences, getContentResolver());
1971            mLocationManager.recordLocation(recordLocation);
1972
1973            // Check if the current effects selection has changed
1974            if (updateEffectSelection()) return;
1975
1976            // Check if camera id is changed.
1977            int cameraId = CameraSettings.readPreferredCameraId(mPreferences);
1978            if (mCameraId != cameraId) {
1979                // Restart the activity to have a crossfade animation.
1980                // TODO: Use SurfaceTexture to implement a better and faster
1981                // animation.
1982                if (mIsVideoCaptureIntent) {
1983                    // If the intent is video capture, stay in video capture mode.
1984                    MenuHelper.gotoVideoMode(this, getIntent());
1985                } else {
1986                    MenuHelper.gotoVideoMode(this);
1987                }
1988                finish();
1989            } else {
1990                readVideoPreferences();
1991                // We need to restart the preview if preview size is changed.
1992                Size size = mParameters.getPreviewSize();
1993                if (size.width != mDesiredPreviewWidth
1994                        || size.height != mDesiredPreviewHeight) {
1995                    if (!effectsActive()) {
1996                        mCameraDevice.stopPreview();
1997                    } else {
1998                        mEffectsRecorder.release();
1999                    }
2000                    resizeForPreviewAspectRatio();
2001                    startPreview(); // Parameters will be set in startPreview().
2002                } else {
2003                    setCameraParameters();
2004                }
2005            }
2006            showTimeLapseUI(mCaptureTimeLapse);
2007        }
2008    }
2009
2010    private boolean updateEffectSelection() {
2011        int previousEffectType = mEffectType;
2012        Object previousEffectParameter = mEffectParameter;
2013        mEffectType = CameraSettings.readEffectType(mPreferences);
2014        mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
2015
2016        if (mEffectType == previousEffectType) {
2017            if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
2018            if (mEffectParameter.equals(previousEffectParameter)) return false;
2019        }
2020        Log.v(TAG, "New effect selection: " + mPreferences.getString(
2021                CameraSettings.KEY_VIDEO_EFFECT, "none"));
2022
2023        if (mEffectType == EffectsRecorder.EFFECT_NONE) {
2024            // Stop effects and return to normal preview
2025            mEffectsRecorder.stopPreview();
2026            return true;
2027        }
2028        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
2029            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
2030            // Request video from gallery to use for background
2031            Intent i = new Intent(Intent.ACTION_PICK);
2032            i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
2033                             "video/*");
2034            i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
2035            startActivityForResult(i, EffectsRecorder.EFFECT_BACKDROPPER);
2036            return true;
2037        }
2038        if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
2039            // Stop regular preview and start effects.
2040            mCameraDevice.stopPreview();
2041            checkQualityAndStartPreview();
2042        } else {
2043            // Switch currently running effect
2044            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
2045        }
2046        return true;
2047    }
2048
2049    // Verifies that the current preview view size is correct before starting
2050    // preview. If not, resets the surface holder and resizes the view.
2051    private void checkQualityAndStartPreview() {
2052        readVideoPreferences();
2053        Size size = mParameters.getPreviewSize();
2054        if (size.width != mDesiredPreviewWidth
2055                || size.height != mDesiredPreviewHeight) {
2056            resizeForPreviewAspectRatio();
2057        } else {
2058            // Start up preview again
2059            startPreview();
2060        }
2061    }
2062
2063    private void showTimeLapseUI(boolean enable) {
2064        if (mTimeLapseLabel != null) {
2065            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
2066        }
2067    }
2068
2069    private void showSharePopup() {
2070        Uri uri = mThumbnail.getUri();
2071        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
2072            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
2073                    mOrientationCompensation, mPreviewPanel);
2074        }
2075        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
2076    }
2077
2078    @Override
2079    public boolean dispatchTouchEvent(MotionEvent m) {
2080        // Check if the popup window should be dismissed first.
2081        if (mPopupGestureDetector != null && mPopupGestureDetector.onTouchEvent(m)) {
2082            return true;
2083        }
2084
2085        return super.dispatchTouchEvent(m);
2086    }
2087
2088    private class PopupGestureListener extends
2089            GestureDetector.SimpleOnGestureListener {
2090        @Override
2091        public boolean onDown(MotionEvent e) {
2092            // Check if the popup window is visible.
2093            View popup = mIndicatorControlContainer.getActiveSettingPopup();
2094            if (popup == null) return false;
2095
2096            // Let popup window or indicator wheel handle the event by
2097            // themselves. Dismiss the popup window if users touch on other
2098            // areas.
2099            if (!Util.pointInView(e.getX(), e.getY(), popup)
2100                    && !Util.pointInView(e.getX(), e.getY(), mIndicatorControlContainer)) {
2101                mIndicatorControlContainer.dismissSettingPopup();
2102                // Let event fall through.
2103            }
2104            return false;
2105        }
2106    }
2107
2108    private class ZoomChangeListener implements ZoomControl.OnZoomChangedListener {
2109        // only for immediate zoom
2110        @Override
2111        public void onZoomValueChanged(int index) {
2112            VideoCamera.this.onZoomValueChanged(index);
2113        }
2114
2115        // only for smooth zoom
2116        @Override
2117        public void onZoomStateChanged(int state) {
2118            if (mPausing) return;
2119
2120            Log.v(TAG, "zoom picker state=" + state);
2121            if (state == ZoomControl.ZOOM_IN) {
2122                VideoCamera.this.onZoomValueChanged(mZoomMax);
2123            } else if (state == ZoomControl.ZOOM_OUT){
2124                VideoCamera.this.onZoomValueChanged(0);
2125            } else {
2126                mTargetZoomValue = -1;
2127                if (mZoomState == ZOOM_START) {
2128                    mZoomState = ZOOM_STOPPING;
2129                    mCameraDevice.stopSmoothZoom();
2130                }
2131            }
2132        }
2133    }
2134
2135    private void initializeZoom() {
2136        mZoomControl = (ZoomControl) findViewById(R.id.zoom_control);
2137        // Get the parameter to make sure we have the up-to-date zoom value.
2138        mParameters = mCameraDevice.getParameters();
2139        if (!mParameters.isZoomSupported()) return;
2140
2141        mZoomMax = mParameters.getMaxZoom();
2142        // Currently we use immediate zoom for fast zooming to get better UX and
2143        // there is no plan to take advantage of the smooth zoom.
2144        mZoomControl.setZoomMax(mZoomMax);
2145        mZoomControl.setZoomIndex(mParameters.getZoom());
2146        mZoomControl.setSmoothZoomSupported(mSmoothZoomSupported);
2147        mZoomControl.setOnZoomChangeListener(new ZoomChangeListener());
2148        mCameraDevice.setZoomChangeListener(mZoomListener);
2149    }
2150
2151    private final class ZoomListener
2152            implements android.hardware.Camera.OnZoomChangeListener {
2153        @Override
2154        public void onZoomChange(int value, boolean stopped, android.hardware.Camera camera) {
2155            Log.v(TAG, "Zoom changed: value=" + value + ". stopped=" + stopped);
2156            mZoomValue = value;
2157
2158            // Update the UI when we get zoom value.
2159            mZoomControl.setZoomIndex(value);
2160
2161            // Keep mParameters up to date. We do not getParameter again in
2162            // takePicture. If we do not do this, wrong zoom value will be set.
2163            mParameters.setZoom(value);
2164
2165            if (stopped && mZoomState != ZOOM_STOPPED) {
2166                if (mTargetZoomValue != -1 && value != mTargetZoomValue) {
2167                    mCameraDevice.startSmoothZoom(mTargetZoomValue);
2168                    mZoomState = ZOOM_START;
2169                } else {
2170                    mZoomState = ZOOM_STOPPED;
2171                }
2172            }
2173        }
2174    }
2175
2176    private void onZoomValueChanged(int index) {
2177        // Not useful to change zoom value when the activity is paused.
2178        if (mPausing) return;
2179
2180        if (mSmoothZoomSupported) {
2181            if (mTargetZoomValue != index && mZoomState != ZOOM_STOPPED) {
2182                mTargetZoomValue = index;
2183                if (mZoomState == ZOOM_START) {
2184                    mZoomState = ZOOM_STOPPING;
2185                    mCameraDevice.stopSmoothZoom();
2186                }
2187            } else if (mZoomState == ZOOM_STOPPED && mZoomValue != index) {
2188                mTargetZoomValue = index;
2189                mCameraDevice.startSmoothZoom(index);
2190                mZoomState = ZOOM_START;
2191            }
2192        } else {
2193            mZoomValue = index;
2194            setCameraParameters();
2195        }
2196    }
2197
2198    private void initializeVideoSnapshot() {
2199        if (mParameters.isVideoSnapshotSupported() && !mIsVideoCaptureIntent) {
2200            findViewById(R.id.camera_preview).setOnTouchListener(this);
2201        }
2202    }
2203
2204    void showVideoSnapshotUI(boolean enabled) {
2205        if (mParameters.isVideoSnapshotSupported() && !mIsVideoCaptureIntent) {
2206            mPreviewFrameLayout.showBorder(enabled);
2207            mIndicatorControlContainer.enableZoom(!enabled);
2208            mShutterButton.setEnabled(!enabled);
2209        }
2210    }
2211
2212    // Preview area is touched. Take a picture.
2213    @Override
2214    public boolean onTouch(View v, MotionEvent e) {
2215        if (mMediaRecorderRecording && effectsActive()) {
2216            Toast.makeText(this, getResources().getString(
2217                    R.string.disable_video_snapshot_hint), Toast.LENGTH_LONG).show();
2218            return false;
2219        }
2220
2221        if (mPausing || mSnapshotInProgress
2222                || !mMediaRecorderRecording || effectsActive()) {
2223            return false;
2224        }
2225
2226        // Set rotation and gps data.
2227        Util.setRotationParameter(mParameters, mCameraId, mOrientation);
2228        Location loc = mLocationManager.getCurrentLocation();
2229        Util.setGpsParameters(mParameters, loc);
2230        mCameraDevice.setParameters(mParameters);
2231
2232        Log.v(TAG, "Video snapshot start");
2233        mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc));
2234        showVideoSnapshotUI(true);
2235        mSnapshotInProgress = true;
2236        return true;
2237    }
2238
2239    private final class JpegPictureCallback implements PictureCallback {
2240        Location mLocation;
2241
2242        public JpegPictureCallback(Location loc) {
2243            mLocation = loc;
2244        }
2245
2246        @Override
2247        public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
2248            Log.v(TAG, "onPictureTaken");
2249            mSnapshotInProgress = false;
2250            showVideoSnapshotUI(false);
2251            storeImage(jpegData, mLocation);
2252        }
2253    }
2254
2255    private void storeImage(final byte[] data, Location loc) {
2256        long dateTaken = System.currentTimeMillis();
2257        String title = Util.createJpegName(dateTaken);
2258        int orientation = Exif.getOrientation(data);
2259        Size s = mParameters.getPictureSize();
2260        Uri uri = Storage.addImage(mContentResolver, title, dateTaken, loc, orientation, data,
2261                s.width, s.height);
2262        if (uri != null) {
2263            // Create a thumbnail whose width is equal or bigger than that of the preview.
2264            int ratio = (int) Math.ceil((double) mParameters.getPictureSize().width
2265                    / mPreviewFrameLayout.getWidth());
2266            int inSampleSize = Integer.highestOneBit(ratio);
2267            mThumbnail = Thumbnail.createThumbnail(data, orientation, inSampleSize, uri);
2268            if (mThumbnail != null) {
2269                mThumbnailView.setBitmap(mThumbnail.getBitmap());
2270            }
2271            // Share popup may still have the reference to the old thumbnail. Clear it.
2272            mSharePopup = null;
2273            Util.broadcastNewPicture(this, uri);
2274        }
2275    }
2276}
2277