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