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