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 + 1) % mNumberOfCameras);
1485                    onSharedPreferenceChanged();
1486                    return true;
1487                }
1488            }).setIcon(android.R.drawable.ic_menu_camera);
1489        }
1490    }
1491
1492    private PreferenceGroup filterPreferenceScreenByIntent(
1493            PreferenceGroup screen) {
1494        Intent intent = getIntent();
1495        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
1496            CameraSettings.removePreferenceFromScreen(screen,
1497                    CameraSettings.KEY_VIDEO_QUALITY);
1498        }
1499
1500        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
1501            CameraSettings.removePreferenceFromScreen(screen,
1502                    CameraSettings.KEY_VIDEO_QUALITY);
1503        }
1504        return screen;
1505    }
1506
1507    // from MediaRecorder.OnErrorListener
1508    public void onError(MediaRecorder mr, int what, int extra) {
1509        Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra);
1510        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
1511            // We may have run out of space on the sdcard.
1512            stopVideoRecording();
1513            updateAndShowStorageHint();
1514        }
1515    }
1516
1517    // from MediaRecorder.OnInfoListener
1518    public void onInfo(MediaRecorder mr, int what, int extra) {
1519        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
1520            if (mMediaRecorderRecording) onStopVideoRecording(true);
1521        } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
1522            if (mMediaRecorderRecording) onStopVideoRecording(true);
1523
1524            // Show the toast.
1525            Toast.makeText(this, R.string.video_reach_size_limit,
1526                    Toast.LENGTH_LONG).show();
1527        }
1528    }
1529
1530    /*
1531     * Make sure we're not recording music playing in the background, ask the
1532     * MediaPlaybackService to pause playback.
1533     */
1534    private void pauseAudioPlayback() {
1535        // Shamelessly copied from MediaPlaybackService.java, which
1536        // should be public, but isn't.
1537        Intent i = new Intent("com.android.music.musicservicecommand");
1538        i.putExtra("command", "pause");
1539
1540        sendBroadcast(i);
1541    }
1542
1543    // For testing.
1544    public boolean isRecording() {
1545        return mMediaRecorderRecording;
1546    }
1547
1548    private void startVideoRecording() {
1549        Log.v(TAG, "startVideoRecording");
1550
1551        updateAndShowStorageHint();
1552        if (mStorageSpace < Storage.LOW_STORAGE_THRESHOLD) {
1553            Log.v(TAG, "Storage issue, ignore the start request");
1554            return;
1555        }
1556
1557        mCurrentVideoUri = null;
1558        if (effectsActive()) {
1559            initializeEffectsRecording();
1560            if (mEffectsRecorder == null) {
1561                Log.e(TAG, "Fail to initialize effect recorder");
1562                return;
1563            }
1564        } else {
1565            initializeRecorder();
1566            if (mMediaRecorder == null) {
1567                Log.e(TAG, "Fail to initialize media recorder");
1568                return;
1569            }
1570        }
1571
1572        pauseAudioPlayback();
1573
1574        if (effectsActive()) {
1575            try {
1576                mEffectsRecorder.startRecording();
1577            } catch (RuntimeException e) {
1578                Log.e(TAG, "Could not start effects recorder. ", e);
1579                releaseEffectsRecorder();
1580                return;
1581            }
1582        } else {
1583            try {
1584                mMediaRecorder.start(); // Recording is now started
1585            } catch (RuntimeException e) {
1586                Log.e(TAG, "Could not start media recorder. ", e);
1587                releaseMediaRecorder();
1588                // If start fails, frameworks will not lock the camera for us.
1589                mCameraDevice.lock();
1590                return;
1591            }
1592        }
1593
1594        enableCameraControls(false);
1595
1596        mMediaRecorderRecording = true;
1597        mRecordingStartTime = SystemClock.uptimeMillis();
1598        showRecordingUI(true);
1599
1600        updateRecordingTime();
1601        keepScreenOn();
1602    }
1603
1604    private void showRecordingUI(boolean recording) {
1605        if (recording) {
1606            mIndicatorControlContainer.dismissSecondLevelIndicator();
1607            if (mThumbnailView != null) mThumbnailView.setEnabled(false);
1608            mShutterButton.setBackgroundResource(R.drawable.btn_shutter_video_recording);
1609            mRecordingTimeView.setText("");
1610            mRecordingTimeView.setVisibility(View.VISIBLE);
1611            if (mReviewControl != null) mReviewControl.setVisibility(View.GONE);
1612            if (mCaptureTimeLapse) {
1613                if (Util.isTabletUI()) {
1614                    ((IndicatorControlWheelContainer) mIndicatorControlContainer)
1615                            .startTimeLapseAnimation(
1616                                    mTimeBetweenTimeLapseFrameCaptureMs,
1617                                    mRecordingStartTime);
1618                }
1619            }
1620        } else {
1621            if (mThumbnailView != null) mThumbnailView.setEnabled(true);
1622            mShutterButton.setBackgroundResource(R.drawable.btn_shutter_video);
1623            mRecordingTimeView.setVisibility(View.GONE);
1624            if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE);
1625            if (mCaptureTimeLapse) {
1626                if (Util.isTabletUI()) {
1627                    ((IndicatorControlWheelContainer) mIndicatorControlContainer)
1628                            .stopTimeLapseAnimation();
1629                }
1630            }
1631        }
1632    }
1633
1634    private void getThumbnail() {
1635        if (mCurrentVideoUri != null) {
1636            Bitmap videoFrame = Thumbnail.createVideoThumbnail(mCurrentVideoFilename,
1637                    mPreviewFrameLayout.getWidth());
1638            if (videoFrame != null) {
1639                mThumbnail = new Thumbnail(mCurrentVideoUri, videoFrame, 0);
1640                mThumbnailView.setBitmap(mThumbnail.getBitmap());
1641                // Share popup may still have the reference to the old thumbnail. Clear it.
1642                mSharePopup = null;
1643            }
1644        }
1645    }
1646
1647    private void showAlert() {
1648        Bitmap bitmap = null;
1649        if (mVideoFileDescriptor != null) {
1650            bitmap = Thumbnail.createVideoThumbnail(mVideoFileDescriptor.getFileDescriptor(),
1651                    mPreviewFrameLayout.getWidth());
1652        } else if (mCurrentVideoFilename != null) {
1653            bitmap = Thumbnail.createVideoThumbnail(mCurrentVideoFilename,
1654                    mPreviewFrameLayout.getWidth());
1655        }
1656        if (bitmap != null) {
1657            // MetadataRetriever already rotates the thumbnail. We should rotate
1658            // it to match the UI orientation (and mirror if it is front-facing camera).
1659            CameraInfo[] info = CameraHolder.instance().getCameraInfo();
1660            boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
1661            bitmap = Util.rotateAndMirror(bitmap, -mOrientationCompensationAtRecordStart,
1662                    mirror);
1663            mReviewImage.setImageBitmap(bitmap);
1664            mReviewImage.setVisibility(View.VISIBLE);
1665        }
1666
1667        Util.fadeOut(mShutterButton);
1668        Util.fadeOut(mIndicatorControlContainer);
1669        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1670        for (int id : pickIds) {
1671            Util.fadeIn(findViewById(id));
1672        }
1673
1674        showTimeLapseUI(false);
1675    }
1676
1677    private void hideAlert() {
1678        mReviewImage.setVisibility(View.GONE);
1679        mShutterButton.setEnabled(true);
1680        enableCameraControls(true);
1681
1682        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1683        for (int id : pickIds) {
1684            Util.fadeOut(findViewById(id));
1685        }
1686        Util.fadeIn(mShutterButton);
1687        Util.fadeIn(mIndicatorControlContainer);
1688
1689        if (mCaptureTimeLapse) {
1690            showTimeLapseUI(true);
1691        }
1692    }
1693
1694    private void stopVideoRecording() {
1695        Log.v(TAG, "stopVideoRecording");
1696        if (mMediaRecorderRecording) {
1697            boolean shouldAddToMediaStoreNow = false;
1698
1699            try {
1700                if (effectsActive()) {
1701                    // This is asynchronous, so we can't add to media store now because thumbnail
1702                    // may not be ready. In such case addVideoToMediaStore is called later
1703                    // through a callback from the MediaEncoderFilter to EffectsRecorder,
1704                    // and then to the VideoCamera.
1705                    mEffectsRecorder.stopRecording();
1706                } else {
1707                    mMediaRecorder.setOnErrorListener(null);
1708                    mMediaRecorder.setOnInfoListener(null);
1709                    mMediaRecorder.stop();
1710                    shouldAddToMediaStoreNow = true;
1711                }
1712                mCurrentVideoFilename = mVideoFilename;
1713                Log.v(TAG, "Setting current video filename: "
1714                        + mCurrentVideoFilename);
1715            } catch (RuntimeException e) {
1716                Log.e(TAG, "stop fail",  e);
1717                if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
1718            }
1719
1720            mMediaRecorderRecording = false;
1721            showRecordingUI(false);
1722            if (!mIsVideoCaptureIntent) {
1723                enableCameraControls(true);
1724            }
1725            // The orientation was fixed during video recording. Now make it
1726            // reflect the device orientation as video recording is stopped.
1727            setOrientationIndicator(mOrientationCompensation);
1728            keepScreenOnAwhile();
1729            if (shouldAddToMediaStoreNow) {
1730                addVideoToMediaStore();
1731            }
1732        }
1733        // always release media recorder
1734        if (!effectsActive()) {
1735            releaseMediaRecorder();
1736        }
1737    }
1738
1739    private void resetScreenOn() {
1740        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1741        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1742    }
1743
1744    private void keepScreenOnAwhile() {
1745        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1746        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1747        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1748    }
1749
1750    private void keepScreenOn() {
1751        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1752        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1753    }
1754
1755    private void initThumbnailButton() {
1756        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
1757        mThumbnailView.enableFilter(false);
1758        mThumbnailView.setVisibility(View.VISIBLE);
1759        // Load the thumbnail from the disk.
1760        mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
1761    }
1762
1763    private void updateThumbnailButton() {
1764        if (mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), mContentResolver)) {
1765            mThumbnail = Thumbnail.getLastThumbnail(mContentResolver);
1766        }
1767        if (mThumbnail != null) {
1768            mThumbnailView.setBitmap(mThumbnail.getBitmap());
1769        } else {
1770            mThumbnailView.setBitmap(null);
1771        }
1772    }
1773
1774    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
1775        long seconds = milliSeconds / 1000; // round down to compute seconds
1776        long minutes = seconds / 60;
1777        long hours = minutes / 60;
1778        long remainderMinutes = minutes - (hours * 60);
1779        long remainderSeconds = seconds - (minutes * 60);
1780
1781        StringBuilder timeStringBuilder = new StringBuilder();
1782
1783        // Hours
1784        if (hours > 0) {
1785            if (hours < 10) {
1786                timeStringBuilder.append('0');
1787            }
1788            timeStringBuilder.append(hours);
1789
1790            timeStringBuilder.append(':');
1791        }
1792
1793        // Minutes
1794        if (remainderMinutes < 10) {
1795            timeStringBuilder.append('0');
1796        }
1797        timeStringBuilder.append(remainderMinutes);
1798        timeStringBuilder.append(':');
1799
1800        // Seconds
1801        if (remainderSeconds < 10) {
1802            timeStringBuilder.append('0');
1803        }
1804        timeStringBuilder.append(remainderSeconds);
1805
1806        // Centi seconds
1807        if (displayCentiSeconds) {
1808            timeStringBuilder.append('.');
1809            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
1810            if (remainderCentiSeconds < 10) {
1811                timeStringBuilder.append('0');
1812            }
1813            timeStringBuilder.append(remainderCentiSeconds);
1814        }
1815
1816        return timeStringBuilder.toString();
1817    }
1818
1819    private long getTimeLapseVideoLength(long deltaMs) {
1820        // For better approximation calculate fractional number of frames captured.
1821        // This will update the video time at a higher resolution.
1822        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
1823        return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
1824    }
1825
1826    private void updateRecordingTime() {
1827        if (!mMediaRecorderRecording) {
1828            return;
1829        }
1830        long now = SystemClock.uptimeMillis();
1831        long delta = now - mRecordingStartTime;
1832
1833        // Starting a minute before reaching the max duration
1834        // limit, we'll countdown the remaining time instead.
1835        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
1836                && delta >= mMaxVideoDurationInMs - 60000);
1837
1838        long deltaAdjusted = delta;
1839        if (countdownRemainingTime) {
1840            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
1841        }
1842        String text;
1843
1844        long targetNextUpdateDelay;
1845        if (!mCaptureTimeLapse) {
1846            text = millisecondToTimeString(deltaAdjusted, false);
1847            targetNextUpdateDelay = 1000;
1848        } else {
1849            // The length of time lapse video is different from the length
1850            // of the actual wall clock time elapsed. Display the video length
1851            // only in format hh:mm:ss.dd, where dd are the centi seconds.
1852            text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
1853            targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
1854        }
1855
1856        mRecordingTimeView.setText(text);
1857
1858        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1859            // Avoid setting the color on every update, do it only
1860            // when it needs changing.
1861            mRecordingTimeCountsDown = countdownRemainingTime;
1862
1863            int color = getResources().getColor(countdownRemainingTime
1864                    ? R.color.recording_time_remaining_text
1865                    : R.color.recording_time_elapsed_text);
1866
1867            mRecordingTimeView.setTextColor(color);
1868        }
1869
1870        long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
1871        mHandler.sendEmptyMessageDelayed(
1872                UPDATE_RECORD_TIME, actualNextUpdateDelay);
1873    }
1874
1875    private static boolean isSupported(String value, List<String> supported) {
1876        return supported == null ? false : supported.indexOf(value) >= 0;
1877    }
1878
1879    private void setCameraParameters() {
1880        mParameters = mCameraDevice.getParameters();
1881
1882        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
1883        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
1884
1885        // Set flash mode.
1886        String flashMode = mPreferences.getString(
1887                CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
1888                getString(R.string.pref_camera_video_flashmode_default));
1889        List<String> supportedFlash = mParameters.getSupportedFlashModes();
1890        if (isSupported(flashMode, supportedFlash)) {
1891            mParameters.setFlashMode(flashMode);
1892        } else {
1893            flashMode = mParameters.getFlashMode();
1894            if (flashMode == null) {
1895                flashMode = getString(
1896                        R.string.pref_camera_flashmode_no_flash);
1897            }
1898        }
1899
1900        // Set white balance parameter.
1901        String whiteBalance = mPreferences.getString(
1902                CameraSettings.KEY_WHITE_BALANCE,
1903                getString(R.string.pref_camera_whitebalance_default));
1904        if (isSupported(whiteBalance,
1905                mParameters.getSupportedWhiteBalance())) {
1906            mParameters.setWhiteBalance(whiteBalance);
1907        } else {
1908            whiteBalance = mParameters.getWhiteBalance();
1909            if (whiteBalance == null) {
1910                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
1911            }
1912        }
1913
1914        // Set zoom.
1915        if (mParameters.isZoomSupported()) {
1916            mParameters.setZoom(mZoomValue);
1917        }
1918
1919        // Set continuous autofocus.
1920        List<String> supportedFocus = mParameters.getSupportedFocusModes();
1921        if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
1922            mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
1923        }
1924
1925        mParameters.setRecordingHint(true);
1926
1927        // Enable video stabilization. Convenience methods not available in API
1928        // level <= 14
1929        String vstabSupported = mParameters.get("video-stabilization-supported");
1930        if ("true".equals(vstabSupported)) {
1931            mParameters.set("video-stabilization", "true");
1932        }
1933
1934        // Set picture size.
1935        // The logic here is different from the logic in still-mode camera.
1936        // There we determine the preview size based on the picture size, but
1937        // here we determine the picture size based on the preview size.
1938        List<Size> supported = mParameters.getSupportedPictureSizes();
1939        Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported,
1940                (double) mDesiredPreviewWidth / mDesiredPreviewHeight);
1941        Size original = mParameters.getPictureSize();
1942        if (!original.equals(optimalSize)) {
1943            mParameters.setPictureSize(optimalSize.width, optimalSize.height);
1944        }
1945        Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" +
1946                optimalSize.height);
1947
1948        // Set JPEG quality.
1949        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
1950                CameraProfile.QUALITY_HIGH);
1951        mParameters.setJpegQuality(jpegQuality);
1952
1953        mCameraDevice.setParameters(mParameters);
1954        // Keep preview size up to date.
1955        mParameters = mCameraDevice.getParameters();
1956    }
1957
1958    private boolean switchToOtherMode(int mode) {
1959        if (isFinishing()) return false;
1960        MenuHelper.gotoMode(mode, this);
1961        finish();
1962        return true;
1963    }
1964
1965    public boolean onModeChanged(int mode) {
1966        if (mode != ModePicker.MODE_VIDEO) {
1967            return switchToOtherMode(mode);
1968        } else {
1969            return true;
1970        }
1971    }
1972
1973    @Override
1974    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1975        switch (requestCode) {
1976            case EffectsRecorder.EFFECT_BACKDROPPER:
1977                if (resultCode == RESULT_OK) {
1978                    // onActivityResult() runs before onResume(), so this parameter will be
1979                    // seen by startPreview from onResume()
1980                    mEffectUriFromGallery = ((Uri) data.getData()).toString();
1981                    Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
1982                    mResetEffect = false;
1983                } else {
1984                    mEffectUriFromGallery = null;
1985                    Log.w(TAG, "No URI from gallery");
1986                    mResetEffect = true;
1987                }
1988                break;
1989            default:
1990                Log.e(TAG, "Unknown activity result sent to Camera!");
1991                break;
1992        }
1993    }
1994
1995    @Override
1996    public void onEffectsUpdate(int effectId, int effectMsg) {
1997        if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
1998            // Effects have shut down. Hide learning message if any,
1999            // and restart regular preview.
2000            mBgLearningMessageFrame.setVisibility(View.GONE);
2001            checkQualityAndStartPreview();
2002        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
2003            // TODO: This assumes the codepath from onStopVideoRecording.  It
2004            // does not appear to cause problems for the other codepaths, but
2005            // should be properly thought through.
2006            if (mEffectsDisplayResult) {
2007                addVideoToMediaStore();
2008                if (mIsVideoCaptureIntent) {
2009                    if (!mQuickCapture) {
2010                        showAlert();
2011                    }
2012                } else {
2013                    getThumbnail();
2014                }
2015            }
2016            mEffectsDisplayResult = false;
2017        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
2018            // Enable the shutter button once the preview is complete.
2019            mShutterButton.setEnabled(true);
2020        } else if (effectId == EffectsRecorder.EFFECT_BACKDROPPER) {
2021            switch (effectMsg) {
2022                case EffectsRecorder.EFFECT_MSG_STARTED_LEARNING:
2023                    mBgLearningMessageFrame.setVisibility(View.VISIBLE);
2024                    break;
2025                case EffectsRecorder.EFFECT_MSG_DONE_LEARNING:
2026                case EffectsRecorder.EFFECT_MSG_SWITCHING_EFFECT:
2027                    mBgLearningMessageFrame.setVisibility(View.GONE);
2028                    break;
2029            }
2030        }
2031    }
2032
2033    public void onCancelBgTraining(View v) {
2034        // Remove training message
2035        mBgLearningMessageFrame.setVisibility(View.GONE);
2036        // Write default effect out to shared prefs
2037        writeDefaultEffectToPrefs();
2038        // Tell the indicator controller to redraw based on new shared pref values
2039        mIndicatorControlContainer.reloadPreferences();
2040        // Tell VideoCamer to re-init based on new shared pref values.
2041        onSharedPreferenceChanged();
2042    }
2043
2044    @Override
2045    public synchronized void onEffectsError(Exception exception, String fileName) {
2046        // TODO: Eventually we may want to show the user an error dialog, and then restart the
2047        // camera and encoder gracefully. For now, we just delete the file and bail out.
2048        if (fileName != null && new File(fileName).exists()) {
2049            deleteVideoFile(fileName);
2050        }
2051        if (exception instanceof MediaRecorderStopException) {
2052            Log.w(TAG, "Problem recoding video file. Removing incomplete file.");
2053            return;
2054        }
2055        throw new RuntimeException("Error during recording!", exception);
2056    }
2057
2058    @Override
2059    public void onConfigurationChanged(Configuration config) {
2060        super.onConfigurationChanged(config);
2061    }
2062
2063    public void onOverriddenPreferencesClicked() {
2064    }
2065
2066    public void onRestorePreferencesClicked() {
2067        Runnable runnable = new Runnable() {
2068            public void run() {
2069                restorePreferences();
2070            }
2071        };
2072        mRotateDialog.showAlertDialog(
2073                getString(R.string.confirm_restore_title),
2074                getString(R.string.confirm_restore_message),
2075                getString(android.R.string.ok), runnable,
2076                getString(android.R.string.cancel), null);
2077    }
2078
2079    private void restorePreferences() {
2080        // Reset the zoom. Zoom value is not stored in preference.
2081        if (mParameters.isZoomSupported()) {
2082            mZoomValue = 0;
2083            setCameraParameters();
2084            mZoomControl.setZoomIndex(0);
2085        }
2086
2087        if (mIndicatorControlContainer != null) {
2088            mIndicatorControlContainer.dismissSettingPopup();
2089            CameraSettings.restorePreferences(this, mPreferences,
2090                    mParameters);
2091            mIndicatorControlContainer.reloadPreferences();
2092            onSharedPreferenceChanged();
2093        }
2094    }
2095
2096    private boolean effectsActive() {
2097        return (mEffectType != EffectsRecorder.EFFECT_NONE);
2098    }
2099
2100    public void onSharedPreferenceChanged() {
2101        // ignore the events after "onPause()" or preview has not started yet
2102        if (mPausing) return;
2103        synchronized (mPreferences) {
2104            // If mCameraDevice is not ready then we can set the parameter in
2105            // startPreview().
2106            if (mCameraDevice == null) return;
2107
2108            boolean recordLocation = RecordLocationPreference.get(
2109                    mPreferences, getContentResolver());
2110            mLocationManager.recordLocation(recordLocation);
2111
2112            // Check if the current effects selection has changed
2113            if (updateEffectSelection()) return;
2114
2115            // Check if camera id is changed.
2116            int cameraId = CameraSettings.readPreferredCameraId(mPreferences);
2117            if (mCameraId != cameraId) {
2118                // Restart the activity to have a crossfade animation.
2119                // TODO: Use SurfaceTexture to implement a better and faster
2120                // animation.
2121                Intent intent;
2122                if (mIsVideoCaptureIntent) {
2123                    // If the intent is video capture, stay in video capture mode.
2124                    intent = getIntent();
2125                } else {
2126                    intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
2127                }
2128                // To maintain the same background in background replacer, we
2129                // need to send the background video uri via the Intent (apart
2130                // from the condition that the effects should not be reset).
2131                intent.putExtra(BACKGROUND_URI_GALLERY_EXTRA, mEffectUriFromGallery);
2132                intent.putExtra(RESET_EFFECT_EXTRA, false);
2133                MenuHelper.gotoVideoMode(this, intent);
2134                finish();
2135            } else {
2136                readVideoPreferences();
2137                showTimeLapseUI(mCaptureTimeLapse);
2138                // We need to restart the preview if preview size is changed.
2139                Size size = mParameters.getPreviewSize();
2140                if (size.width != mDesiredPreviewWidth
2141                        || size.height != mDesiredPreviewHeight) {
2142                    if (!effectsActive()) {
2143                        mCameraDevice.stopPreview();
2144                    } else {
2145                        mEffectsRecorder.release();
2146                    }
2147                    resizeForPreviewAspectRatio();
2148                    startPreview(); // Parameters will be set in startPreview().
2149                } else {
2150                    setCameraParameters();
2151                }
2152            }
2153        }
2154    }
2155
2156    private boolean updateEffectSelection() {
2157        int previousEffectType = mEffectType;
2158        Object previousEffectParameter = mEffectParameter;
2159        mEffectType = CameraSettings.readEffectType(mPreferences);
2160        mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
2161
2162        if (mEffectType == previousEffectType) {
2163            if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
2164            if (mEffectParameter.equals(previousEffectParameter)) return false;
2165        }
2166        Log.v(TAG, "New effect selection: " + mPreferences.getString(
2167                CameraSettings.KEY_VIDEO_EFFECT, "none"));
2168
2169        if (mEffectType == EffectsRecorder.EFFECT_NONE) {
2170            // Stop effects and return to normal preview
2171            mEffectsRecorder.stopPreview();
2172            return true;
2173        }
2174        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
2175            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
2176            // Request video from gallery to use for background
2177            Intent i = new Intent(Intent.ACTION_PICK);
2178            i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
2179                             "video/*");
2180            i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
2181            startActivityForResult(i, EffectsRecorder.EFFECT_BACKDROPPER);
2182            return true;
2183        }
2184        if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
2185            // Stop regular preview and start effects.
2186            mCameraDevice.stopPreview();
2187            checkQualityAndStartPreview();
2188        } else {
2189            // Switch currently running effect
2190            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
2191        }
2192        return true;
2193    }
2194
2195    // Verifies that the current preview view size is correct before starting
2196    // preview. If not, resets the surface holder and resizes the view.
2197    private void checkQualityAndStartPreview() {
2198        readVideoPreferences();
2199        showTimeLapseUI(mCaptureTimeLapse);
2200        Size size = mParameters.getPreviewSize();
2201        if (size.width != mDesiredPreviewWidth
2202                || size.height != mDesiredPreviewHeight) {
2203            resizeForPreviewAspectRatio();
2204        } else {
2205            // Start up preview again
2206            startPreview();
2207        }
2208    }
2209
2210    private void showTimeLapseUI(boolean enable) {
2211        if (mTimeLapseLabel != null) {
2212            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
2213        }
2214    }
2215
2216    private void showSharePopup() {
2217        Uri uri = mThumbnail.getUri();
2218        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
2219            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
2220                    mOrientationCompensation, mPreviewPanel);
2221        }
2222        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
2223    }
2224
2225    @Override
2226    public boolean dispatchTouchEvent(MotionEvent m) {
2227        // Check if the popup window should be dismissed first.
2228        if (mPopupGestureDetector != null && mPopupGestureDetector.onTouchEvent(m)) {
2229            return true;
2230        }
2231
2232        return super.dispatchTouchEvent(m);
2233    }
2234
2235    private class PopupGestureListener extends
2236            GestureDetector.SimpleOnGestureListener {
2237        @Override
2238        public boolean onDown(MotionEvent e) {
2239            // Check if the popup window is visible.
2240            View popup = mIndicatorControlContainer.getActiveSettingPopup();
2241            if (popup == null) return false;
2242
2243            // Let popup window or indicator wheel handle the event by
2244            // themselves. Dismiss the popup window if users touch on other
2245            // areas.
2246            if (!Util.pointInView(e.getX(), e.getY(), popup)
2247                    && !Util.pointInView(e.getX(), e.getY(), mIndicatorControlContainer)) {
2248                mIndicatorControlContainer.dismissSettingPopup();
2249                // Let event fall through.
2250            }
2251            return false;
2252        }
2253    }
2254
2255    private class ZoomChangeListener implements ZoomControl.OnZoomChangedListener {
2256        // only for immediate zoom
2257        @Override
2258        public void onZoomValueChanged(int index) {
2259            VideoCamera.this.onZoomValueChanged(index);
2260        }
2261
2262        // only for smooth zoom
2263        @Override
2264        public void onZoomStateChanged(int state) {
2265            if (mPausing) return;
2266
2267            Log.v(TAG, "zoom picker state=" + state);
2268            if (state == ZoomControl.ZOOM_IN) {
2269                VideoCamera.this.onZoomValueChanged(mZoomMax);
2270            } else if (state == ZoomControl.ZOOM_OUT){
2271                VideoCamera.this.onZoomValueChanged(0);
2272            } else {
2273                mTargetZoomValue = -1;
2274                if (mZoomState == ZOOM_START) {
2275                    mZoomState = ZOOM_STOPPING;
2276                    mCameraDevice.stopSmoothZoom();
2277                }
2278            }
2279        }
2280    }
2281
2282    private void initializeZoom() {
2283        mZoomControl = (ZoomControl) findViewById(R.id.zoom_control);
2284        // Get the parameter to make sure we have the up-to-date zoom value.
2285        mParameters = mCameraDevice.getParameters();
2286        if (!mParameters.isZoomSupported()) return;
2287
2288        mZoomMax = mParameters.getMaxZoom();
2289        // Currently we use immediate zoom for fast zooming to get better UX and
2290        // there is no plan to take advantage of the smooth zoom.
2291        mZoomControl.setZoomMax(mZoomMax);
2292        mZoomControl.setZoomIndex(mParameters.getZoom());
2293        mZoomControl.setSmoothZoomSupported(mSmoothZoomSupported);
2294        mZoomControl.setOnZoomChangeListener(new ZoomChangeListener());
2295        mCameraDevice.setZoomChangeListener(mZoomListener);
2296    }
2297
2298    private final class ZoomListener
2299            implements android.hardware.Camera.OnZoomChangeListener {
2300        @Override
2301        public void onZoomChange(int value, boolean stopped, android.hardware.Camera camera) {
2302            Log.v(TAG, "Zoom changed: value=" + value + ". stopped=" + stopped);
2303            mZoomValue = value;
2304
2305            // Update the UI when we get zoom value.
2306            mZoomControl.setZoomIndex(value);
2307
2308            // Keep mParameters up to date. We do not getParameter again in
2309            // takePicture. If we do not do this, wrong zoom value will be set.
2310            mParameters.setZoom(value);
2311
2312            if (stopped && mZoomState != ZOOM_STOPPED) {
2313                if (mTargetZoomValue != -1 && value != mTargetZoomValue) {
2314                    mCameraDevice.startSmoothZoom(mTargetZoomValue);
2315                    mZoomState = ZOOM_START;
2316                } else {
2317                    mZoomState = ZOOM_STOPPED;
2318                }
2319            }
2320        }
2321    }
2322
2323    private void onZoomValueChanged(int index) {
2324        // Not useful to change zoom value when the activity is paused.
2325        if (mPausing) return;
2326
2327        if (mSmoothZoomSupported) {
2328            if (mTargetZoomValue != index && mZoomState != ZOOM_STOPPED) {
2329                mTargetZoomValue = index;
2330                if (mZoomState == ZOOM_START) {
2331                    mZoomState = ZOOM_STOPPING;
2332                    mCameraDevice.stopSmoothZoom();
2333                }
2334            } else if (mZoomState == ZOOM_STOPPED && mZoomValue != index) {
2335                mTargetZoomValue = index;
2336                mCameraDevice.startSmoothZoom(index);
2337                mZoomState = ZOOM_START;
2338            }
2339        } else {
2340            mZoomValue = index;
2341            setCameraParameters();
2342        }
2343    }
2344
2345    private void initializeVideoSnapshot() {
2346        if (mParameters.isVideoSnapshotSupported() && !mIsVideoCaptureIntent) {
2347            findViewById(R.id.camera_preview).setOnTouchListener(this);
2348            // Show the tap to focus toast if this is the first start.
2349            if (mPreferences.getBoolean(
2350                        CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) {
2351                // Delay the toast for one second to wait for orientation.
2352                mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000);
2353            }
2354        }
2355    }
2356
2357    void showVideoSnapshotUI(boolean enabled) {
2358        if (mParameters.isVideoSnapshotSupported() && !mIsVideoCaptureIntent) {
2359            mPreviewFrameLayout.showBorder(enabled);
2360            mIndicatorControlContainer.enableZoom(!enabled);
2361            mShutterButton.setEnabled(!enabled);
2362        }
2363    }
2364
2365    // Preview area is touched. Take a picture.
2366    @Override
2367    public boolean onTouch(View v, MotionEvent e) {
2368        if (mMediaRecorderRecording && effectsActive()) {
2369            new RotateTextToast(this, R.string.disable_video_snapshot_hint,
2370                    mOrientation).show();
2371            return false;
2372        }
2373
2374        if (mPausing || mSnapshotInProgress
2375                || !mMediaRecorderRecording || effectsActive()) {
2376            return false;
2377        }
2378
2379        // Set rotation and gps data.
2380        Util.setRotationParameter(mParameters, mCameraId, mOrientation);
2381        Location loc = mLocationManager.getCurrentLocation();
2382        Util.setGpsParameters(mParameters, loc);
2383        mCameraDevice.setParameters(mParameters);
2384
2385        Log.v(TAG, "Video snapshot start");
2386        mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc));
2387        showVideoSnapshotUI(true);
2388        mSnapshotInProgress = true;
2389        return true;
2390    }
2391
2392    private final class JpegPictureCallback implements PictureCallback {
2393        Location mLocation;
2394
2395        public JpegPictureCallback(Location loc) {
2396            mLocation = loc;
2397        }
2398
2399        @Override
2400        public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
2401            Log.v(TAG, "onPictureTaken");
2402            mSnapshotInProgress = false;
2403            showVideoSnapshotUI(false);
2404            storeImage(jpegData, mLocation);
2405        }
2406    }
2407
2408    private void storeImage(final byte[] data, Location loc) {
2409        long dateTaken = System.currentTimeMillis();
2410        String title = Util.createJpegName(dateTaken);
2411        int orientation = Exif.getOrientation(data);
2412        Size s = mParameters.getPictureSize();
2413        Uri uri = Storage.addImage(mContentResolver, title, dateTaken, loc, orientation, data,
2414                s.width, s.height);
2415        if (uri != null) {
2416            // Create a thumbnail whose width is equal or bigger than that of the preview.
2417            int ratio = (int) Math.ceil((double) mParameters.getPictureSize().width
2418                    / mPreviewFrameLayout.getWidth());
2419            int inSampleSize = Integer.highestOneBit(ratio);
2420            mThumbnail = Thumbnail.createThumbnail(data, orientation, inSampleSize, uri);
2421            if (mThumbnail != null) {
2422                mThumbnailView.setBitmap(mThumbnail.getBitmap());
2423            }
2424            // Share popup may still have the reference to the old thumbnail. Clear it.
2425            mSharePopup = null;
2426            Util.broadcastNewPicture(this, uri);
2427        }
2428    }
2429
2430    private boolean resetEffect() {
2431        if (mResetEffect) {
2432            String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT,
2433                    mPrefVideoEffectDefault);
2434            if (!mPrefVideoEffectDefault.equals(value)) {
2435                writeDefaultEffectToPrefs();
2436                return true;
2437            }
2438        }
2439        mResetEffect = true;
2440        return false;
2441    }
2442
2443    private String convertOutputFormatToMimeType(int outputFileFormat) {
2444        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
2445            return "video/mp4";
2446        }
2447        return "video/3gpp";
2448    }
2449
2450    private String convertOutputFormatToFileExt(int outputFileFormat) {
2451        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
2452            return ".mp4";
2453        }
2454        return ".3gp";
2455    }
2456
2457    private void closeVideoFileDescriptor() {
2458        if (mVideoFileDescriptor != null) {
2459            try {
2460                mVideoFileDescriptor.close();
2461            } catch (IOException e) {
2462                Log.e(TAG, "Fail to close fd", e);
2463            }
2464            mVideoFileDescriptor = null;
2465        }
2466    }
2467
2468    private void showTapToSnapshotToast() {
2469        new RotateTextToast(this, R.string.video_snapshot_hint, mOrientation)
2470                .show();
2471        // Clear the preference.
2472        Editor editor = mPreferences.edit();
2473        editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false);
2474        editor.apply();
2475    }
2476}
2477