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