VideoCamera.java revision 067fe861cf3402f2bef3f83995579d3411f0a8b3
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.gallery.IImage;
20import com.android.camera.gallery.IImageList;
21import com.android.camera.ui.CamcorderHeadUpDisplay;
22import com.android.camera.ui.CameraPicker;
23import com.android.camera.ui.ControlPanel;
24import com.android.camera.ui.GLRootView;
25import com.android.camera.ui.GLView;
26import com.android.camera.ui.HeadUpDisplay;
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.SharedPreferences;
36import android.content.res.Configuration;
37import android.content.res.Resources;
38import android.database.Cursor;
39import android.graphics.Bitmap;
40import android.graphics.drawable.Drawable;
41import android.hardware.Camera.CameraInfo;
42import android.hardware.Camera.Parameters;
43import android.hardware.Camera.Size;
44import android.media.CamcorderProfile;
45import android.media.MediaRecorder;
46import android.media.ThumbnailUtils;
47import android.net.Uri;
48import android.os.Build;
49import android.os.Bundle;
50import android.os.Environment;
51import android.os.Handler;
52import android.os.ParcelFileDescriptor;
53import android.os.Message;
54import android.os.StatFs;
55import android.os.SystemClock;
56import android.provider.MediaStore;
57import android.provider.MediaStore.Video;
58import android.provider.MediaStore.Video.VideoColumns;
59import android.provider.MediaStore.Video.Media;
60import android.provider.Settings;
61import android.util.Log;
62import android.view.Display;
63import android.view.GestureDetector;
64import android.view.KeyEvent;
65import android.view.LayoutInflater;
66import android.view.Menu;
67import android.view.MenuItem;
68import android.view.MotionEvent;
69import android.view.OrientationEventListener;
70import android.view.SurfaceHolder;
71import android.view.SurfaceView;
72import android.view.View;
73import android.view.ViewGroup;
74import android.view.Window;
75import android.view.WindowManager;
76import android.view.MenuItem.OnMenuItemClickListener;
77import android.view.animation.AlphaAnimation;
78import android.view.animation.Animation;
79import android.widget.ImageView;
80import android.widget.TextView;
81import android.widget.Toast;
82
83import java.io.File;
84import java.io.IOException;
85import java.text.SimpleDateFormat;
86import java.util.ArrayList;
87import java.util.Date;
88import java.util.Iterator;
89import java.util.List;
90
91/**
92 * The Camcorder activity.
93 */
94public class VideoCamera extends NoSearchActivity
95        implements View.OnClickListener,
96        ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback,
97        MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener,
98        Switcher.OnSwitchListener, PreviewFrameLayout.OnSizeChangedListener {
99
100    private static final String TAG = "videocamera";
101
102    private static final int CLEAR_SCREEN_DELAY = 4;
103    private static final int UPDATE_RECORD_TIME = 5;
104    private static final int ENABLE_SHUTTER_BUTTON = 6;
105
106    private static final int SCREEN_DELAY = 2 * 60 * 1000;
107
108    // The brightness settings used when it is set to automatic in the system.
109    // The reason why it is set to 0.7 is just because 1.0 is too bright.
110    private static final float DEFAULT_CAMERA_BRIGHTNESS = 0.7f;
111
112    private static final long NO_STORAGE_ERROR = -1L;
113    private static final long CANNOT_STAT_ERROR = -2L;
114    private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L;
115
116    private static final int STORAGE_STATUS_OK = 0;
117    private static final int STORAGE_STATUS_LOW = 1;
118    private static final int STORAGE_STATUS_NONE = 2;
119    private static final int STORAGE_STATUS_FAIL = 3;
120
121    private static final boolean SWITCH_CAMERA = true;
122    private static final boolean SWITCH_VIDEO = false;
123
124    private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms
125
126    /**
127     * An unpublished intent flag requesting to start recording straight away
128     * and return as soon as recording is stopped.
129     * TODO: consider publishing by moving into MediaStore.
130     */
131    private final static String EXTRA_QUICK_CAPTURE =
132            "android.intent.extra.quickCapture";
133
134    private android.hardware.Camera mCameraDevice;
135    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
136
137    private ComboPreferences mPreferences;
138    private PreferenceGroup mPreferenceGroup;
139
140    private PreviewFrameLayout mPreviewFrameLayout;
141    private SurfaceView mVideoPreview;
142    private SurfaceHolder mSurfaceHolder = null;
143    private ImageView mVideoFrame;
144    private GLRootView mGLRootView;
145    // xlarge devices use control panel. Other devices use head-up display.
146    private CamcorderHeadUpDisplay mHeadUpDisplay;
147    private ControlPanel mControlPanel;
148    private MenuItem mSwitchTimeLapseMenuItem;
149    // Front/back camera picker for xlarge layout.
150    private CameraPicker mCameraPicker;
151
152    private boolean mIsVideoCaptureIntent;
153    private boolean mQuickCapture;
154
155    // The last recorded video.
156    private RotateImageView mThumbnailButton;
157
158    private boolean mOpenCameraFail = false;
159
160    private int mStorageStatus = STORAGE_STATUS_OK;
161
162    private MediaRecorder mMediaRecorder;
163    private boolean mMediaRecorderRecording = false;
164    private long mRecordingStartTime;
165    // The video file that the hardware camera is about to record into
166    // (or is recording into.)
167    private String mVideoFilename;
168    private ParcelFileDescriptor mVideoFileDescriptor;
169
170    // The video file that has already been recorded, and that is being
171    // examined by the user.
172    private String mCurrentVideoFilename;
173    private Uri mCurrentVideoUri;
174    private ContentValues mCurrentVideoValues;
175
176    private CamcorderProfile mProfile;
177
178    // The video duration limit. 0 menas no limit.
179    private int mMaxVideoDurationInMs;
180
181    // Time Lapse parameters.
182    private boolean mCaptureTimeLapse = false;
183    private int mTimeBetweenTimeLapseFrameCaptureMs = 2000;
184
185    private int mDesiredPreviewWidth;
186    private int mDesiredPreviewHeight;
187
188    boolean mPausing = false;
189    boolean mPreviewing = false; // True if preview is started.
190
191    private ContentResolver mContentResolver;
192
193    private ShutterButton mShutterButton;
194    private TextView mRecordingTimeView, mTimeLapseRecordingTimeView;
195    private SwitcherSet mSwitcher;
196    private boolean mRecordingTimeCountsDown = false;
197
198    private final ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
199
200    private final Handler mHandler = new MainHandler();
201    private Parameters mParameters;
202
203    // multiple cameras support
204    private int mNumberOfCameras;
205    private int mCameraId;
206    private int mFrontCameraId;
207    private int mBackCameraId;
208
209    private GestureDetector mPopupGestureDetector;
210
211    private MyOrientationEventListener mOrientationListener;
212    // The device orientation in degrees. Default is unknown.
213    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
214    // The orientation compensation for icons and thumbnails.
215    private int mOrientationCompensation = 0;
216    private int mOrientationHint; // the orientation hint for video playback
217
218    // This Handler is used to post message back onto the main thread of the
219    // application
220    private class MainHandler extends Handler {
221        @Override
222        public void handleMessage(Message msg) {
223            switch (msg.what) {
224
225                case ENABLE_SHUTTER_BUTTON:
226                    mShutterButton.setEnabled(true);
227                    break;
228
229                case CLEAR_SCREEN_DELAY: {
230                    getWindow().clearFlags(
231                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
232                    break;
233                }
234
235                case UPDATE_RECORD_TIME: {
236                    updateRecordingTime();
237                    break;
238                }
239
240                default:
241                    Log.v(TAG, "Unhandled message: " + msg.what);
242                    break;
243            }
244        }
245    }
246
247    private BroadcastReceiver mReceiver = null;
248
249    private class MyBroadcastReceiver extends BroadcastReceiver {
250        @Override
251        public void onReceive(Context context, Intent intent) {
252            String action = intent.getAction();
253            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
254                updateAndShowStorageHint(false);
255                stopVideoRecording();
256            } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
257                updateAndShowStorageHint(true);
258                updateThumbnailButton();
259            } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
260                // SD card unavailable
261                // handled in ACTION_MEDIA_EJECT
262            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
263                Toast.makeText(VideoCamera.this,
264                        getResources().getString(R.string.wait), 5000);
265            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
266                updateAndShowStorageHint(true);
267            }
268        }
269    }
270
271    private String createName(long dateTaken) {
272        Date date = new Date(dateTaken);
273        SimpleDateFormat dateFormat = new SimpleDateFormat(
274                getString(R.string.video_file_name_format));
275
276        return dateFormat.format(date);
277    }
278
279    private void showCameraErrorAndFinish() {
280        Resources ress = getResources();
281        Util.showFatalErrorAndFinish(VideoCamera.this,
282                ress.getString(R.string.camera_error_title),
283                ress.getString(R.string.cannot_connect_camera));
284    }
285
286    @Override
287    public void onCreate(Bundle icicle) {
288        super.onCreate(icicle);
289
290        Window win = getWindow();
291
292        // Overright the brightness settings if it is automatic
293        int mode = Settings.System.getInt(
294                getContentResolver(),
295                Settings.System.SCREEN_BRIGHTNESS_MODE,
296                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
297        if (mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
298            WindowManager.LayoutParams winParams = win.getAttributes();
299            winParams.screenBrightness = DEFAULT_CAMERA_BRIGHTNESS;
300            win.setAttributes(winParams);
301        }
302
303        mPreferences = new ComboPreferences(this);
304        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
305        mCameraId = CameraSettings.readPreferredCameraId(mPreferences);
306        mPreferences.setLocalId(this, mCameraId);
307        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
308
309        mNumberOfCameras = CameraHolder.instance().getNumberOfCameras();
310
311        /*
312         * To reduce startup time, we start the preview in another thread.
313         * We make sure the preview is started at the end of onCreate.
314         */
315        Thread startPreviewThread = new Thread(new Runnable() {
316            public void run() {
317                mOpenCameraFail = !openCamera();
318                // In eng build, we throw the exception so that test tool
319                // can detect it and report it
320                if (mOpenCameraFail && "eng".equals(Build.TYPE)) {
321                    throw new RuntimeException("openCamera failed");
322                }
323                readVideoPreferences();
324                startPreview();
325            }
326        });
327        startPreviewThread.start();
328
329        mContentResolver = getContentResolver();
330
331        requestWindowFeature(Window.FEATURE_PROGRESS);
332        mIsVideoCaptureIntent = isVideoCaptureIntent();
333        if (mIsVideoCaptureIntent) {
334            setContentView(R.layout.video_camera_attach);
335
336            View controlBar = findViewById(R.id.control_bar);
337            controlBar.findViewById(R.id.btn_cancel).setOnClickListener(this);
338            ImageView retake =
339                    (ImageView) controlBar.findViewById(R.id.btn_retake);
340            retake.setOnClickListener(this);
341            retake.setImageResource(R.drawable.btn_ic_review_retake_video);
342            controlBar.findViewById(R.id.btn_play).setOnClickListener(this);
343            controlBar.findViewById(R.id.btn_done).setOnClickListener(this);
344        } else {
345            setContentView(R.layout.video_camera);
346
347            initThumbnailButton();
348            mSwitcher = (SwitcherSet) findViewById(R.id.camera_switch);
349            mSwitcher.setVisibility(View.VISIBLE);
350            mSwitcher.setOnSwitchListener(this);
351        }
352
353        mPreviewFrameLayout = (PreviewFrameLayout)
354                findViewById(R.id.frame_layout);
355        mPreviewFrameLayout.setOnSizeChangedListener(this);
356
357        mVideoPreview = (SurfaceView) findViewById(R.id.camera_preview);
358        mVideoFrame = (ImageView) findViewById(R.id.video_frame);
359
360        // don't set mSurfaceHolder here. We have it set ONLY within
361        // surfaceCreated / surfaceDestroyed, other parts of the code
362        // assume that when it is set, the surface is also set.
363        SurfaceHolder holder = mVideoPreview.getHolder();
364        holder.addCallback(this);
365        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
366
367        mQuickCapture = getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
368
369        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
370        mShutterButton.setImageResource(R.drawable.btn_ic_video_record);
371        mShutterButton.setOnShutterButtonListener(this);
372        mShutterButton.requestFocus();
373
374        mRecordingTimeView = (TextView) findViewById(R.id.recording_time);
375        mTimeLapseRecordingTimeView = (TextView) findViewById(
376                R.id.time_lapse_recording_time);
377        mOrientationListener = new MyOrientationEventListener(VideoCamera.this);
378
379        // Make sure preview is started.
380        try {
381            startPreviewThread.join();
382            if (mOpenCameraFail) {
383                showCameraErrorAndFinish();
384                return;
385            }
386        } catch (InterruptedException ex) {
387            // ignore
388        }
389
390        resizeForPreviewAspectRatio();
391
392        // Initialize after startPreview becuase this need mParameters.
393        initializeControlPanel();
394        // xlarge devices use control panel. Other devices use head-up display.
395        if (mControlPanel == null) {
396            mHeadUpDisplay = new CamcorderHeadUpDisplay(this);
397            mHeadUpDisplay.setListener(new MyHeadUpDisplayListener());
398            initializeHeadUpDisplay();
399        }
400        initializeCameraPicker();
401    }
402
403    private void changeHeadUpDisplayState() {
404        if (mHeadUpDisplay == null) return;
405        // If the camera resumes behind the lock screen, the orientation
406        // will be portrait. That causes OOM when we try to allocation GPU
407        // memory for the GLSurfaceView again when the orientation changes. So,
408        // we delayed initialization of HeadUpDisplay until the orientation
409        // becomes landscape.
410        Configuration config = getResources().getConfiguration();
411        if (config.orientation == Configuration.ORIENTATION_LANDSCAPE
412                && !mPausing && mGLRootView == null) {
413            attachHeadUpDisplay();
414        } else if (mGLRootView != null) {
415            detachHeadUpDisplay();
416        }
417    }
418
419    private void initializeCameraPicker() {
420        mCameraPicker = (CameraPicker) findViewById(R.id.camera_picker);
421        if (mCameraPicker != null) {
422            mCameraPicker.setImageResource(R.drawable.camera_toggle_video);
423            ListPreference pref = mPreferenceGroup.findPreference(
424                    CameraSettings.KEY_CAMERA_ID);
425            if (pref != null) {
426                mCameraPicker.initialize(pref);
427                mCameraPicker.setListener(new MyCameraPickerListener());
428            }
429        }
430    }
431
432    private void loadCameraPreferences() {
433        CameraSettings settings = new CameraSettings(this, mParameters,
434                mCameraId, CameraHolder.instance().getCameraInfo());
435
436        // Only need to load camera ID's once
437        if (mPreferenceGroup == null) {
438            mPreferenceGroup = settings.getPreferenceGroup(R.xml.video_preferences);
439            mFrontCameraId =
440                    settings.getCameraIdByIndex(CameraInfo.CAMERA_FACING_FRONT);
441            mBackCameraId =
442                    settings.getCameraIdByIndex(CameraInfo.CAMERA_FACING_BACK);
443        } else {
444            mPreferenceGroup = settings.getPreferenceGroup(R.xml.video_preferences);
445        }
446    }
447
448    private void initializeHeadUpDisplay() {
449        if (mHeadUpDisplay == null) return;
450        loadCameraPreferences();
451
452        if (mIsVideoCaptureIntent) {
453            mPreferenceGroup = filterPreferenceScreenByIntent(mPreferenceGroup);
454        }
455        mHeadUpDisplay.initialize(this, mPreferenceGroup,
456                mOrientationCompensation, mCaptureTimeLapse);
457    }
458
459    private void attachHeadUpDisplay() {
460        mHeadUpDisplay.setOrientation(mOrientationCompensation);
461        ViewGroup frame = (ViewGroup) findViewById(R.id.frame);
462        mGLRootView = new GLRootView(this);
463        frame.addView(mGLRootView);
464        mGLRootView.setContentPane(mHeadUpDisplay);
465    }
466
467    private void detachHeadUpDisplay() {
468        mHeadUpDisplay.collapse();
469        ((ViewGroup) mGLRootView.getParent()).removeView(mGLRootView);
470        mGLRootView = null;
471    }
472
473    private boolean collapseCameraControls() {
474        if (mHeadUpDisplay != null && mHeadUpDisplay.collapse()) {
475            return true;
476        }
477        if (mControlPanel != null && mControlPanel.dismissSettingPopup()) {
478            return true;
479        }
480        return false;
481    }
482
483    private void enableCameraControls(boolean enable) {
484        if (mHeadUpDisplay != null) mHeadUpDisplay.setEnabled(enable);
485        if (mControlPanel != null) mControlPanel.setEnabled(enable);
486    }
487
488    private void initializeControlPanel() {
489        mControlPanel = (ControlPanel) findViewById(R.id.control_panel);
490        if (mControlPanel == null) return;
491        loadCameraPreferences();
492
493        String[] keys = new String[]{CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
494            CameraSettings.KEY_WHITE_BALANCE,
495            CameraSettings.KEY_COLOR_EFFECT,
496            CameraSettings.KEY_VIDEO_QUALITY};
497        mControlPanel.initialize(this, mPreferenceGroup, keys, false);
498        mControlPanel.setListener(new MyControlPanelListener());
499        mPopupGestureDetector = new GestureDetector(this,
500                new PopupGestureListener());
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            if (mMediaRecorderRecording) return;
516            // We keep the last known orientation. So if the user first orient
517            // the camera then point the camera to floor or sky, we still have
518            // the correct orientation.
519            if (orientation == ORIENTATION_UNKNOWN) return;
520            mOrientation = roundOrientation(orientation);
521            // When the screen is unlocked, display rotation may change. Always
522            // calculate the up-to-date orientationCompensation.
523            int orientationCompensation = mOrientation
524                    + Util.getDisplayRotation(VideoCamera.this);
525            if (mOrientationCompensation != orientationCompensation) {
526                mOrientationCompensation = orientationCompensation;
527                if (!mIsVideoCaptureIntent) {
528                    setOrientationIndicator(mOrientationCompensation);
529                }
530                if (mHeadUpDisplay != null) {
531                    mHeadUpDisplay.setOrientation(mOrientationCompensation);
532                }
533            }
534        }
535    }
536
537    private void setOrientationIndicator(int degree) {
538        RotateImageView icon = (RotateImageView) findViewById(
539                R.id.review_thumbnail);
540        if (icon != null) icon.setDegree(degree);
541
542        icon = (RotateImageView) findViewById(R.id.camera_switch_icon);
543        if (icon != null) icon.setDegree(degree);
544        icon = (RotateImageView) findViewById(R.id.video_switch_icon);
545        if (icon != null) icon.setDegree(degree);
546    }
547
548    @Override
549    protected void onStart() {
550        super.onStart();
551        if (!mIsVideoCaptureIntent) {
552            mSwitcher.setSwitch(SWITCH_VIDEO);
553        }
554    }
555
556    private void startPlayVideoActivity() {
557        Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri);
558        try {
559            startActivity(intent);
560        } catch (android.content.ActivityNotFoundException ex) {
561            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
562        }
563    }
564
565    public void onClick(View v) {
566        switch (v.getId()) {
567            case R.id.btn_retake:
568                deleteCurrentVideo();
569                hideAlert();
570                break;
571            case R.id.btn_play:
572                startPlayVideoActivity();
573                break;
574            case R.id.btn_done:
575                doReturnToCaller(true);
576                break;
577            case R.id.btn_cancel:
578                stopVideoRecording();
579                doReturnToCaller(false);
580                break;
581            case R.id.review_thumbnail:
582                if (!mMediaRecorderRecording) viewVideo(mThumbnailButton);
583                break;
584            case R.id.btn_gallery:
585                gotoGallery();
586                break;
587        }
588    }
589
590    public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
591        // Do nothing (everything happens in onShutterButtonClick).
592    }
593
594    private void onStopVideoRecording(boolean valid) {
595        stopVideoRecording();
596        if (mIsVideoCaptureIntent) {
597            if (mQuickCapture) {
598                doReturnToCaller(valid);
599            } else {
600                showAlert();
601            }
602        } else {
603            getThumbnail();
604        }
605    }
606
607    public void onShutterButtonClick(ShutterButton button) {
608        switch (button.getId()) {
609            case R.id.shutter_button:
610                if (collapseCameraControls()) return;
611
612                if (mMediaRecorderRecording) {
613                    onStopVideoRecording(true);
614                } else {
615                    startVideoRecording();
616                }
617                mShutterButton.setEnabled(false);
618                mHandler.sendEmptyMessageDelayed(
619                        ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
620                break;
621        }
622    }
623
624    private OnScreenHint mStorageHint;
625
626    private void updateAndShowStorageHint(boolean mayHaveSd) {
627        mStorageStatus = getStorageStatus(mayHaveSd);
628        showStorageHint();
629    }
630
631    private void showStorageHint() {
632        String errorMessage = null;
633        switch (mStorageStatus) {
634            case STORAGE_STATUS_NONE:
635                errorMessage = getString(R.string.no_storage);
636                break;
637            case STORAGE_STATUS_LOW:
638                errorMessage = getString(R.string.spaceIsLow_content);
639                break;
640            case STORAGE_STATUS_FAIL:
641                errorMessage = getString(R.string.access_sd_fail);
642                break;
643        }
644        if (errorMessage != null) {
645            if (mStorageHint == null) {
646                mStorageHint = OnScreenHint.makeText(this, errorMessage);
647            } else {
648                mStorageHint.setText(errorMessage);
649            }
650            mStorageHint.show();
651        } else if (mStorageHint != null) {
652            mStorageHint.cancel();
653            mStorageHint = null;
654        }
655    }
656
657    private int getStorageStatus(boolean mayHaveSd) {
658        long remaining = mayHaveSd ? getAvailableStorage() : NO_STORAGE_ERROR;
659        if (remaining == NO_STORAGE_ERROR) {
660            return STORAGE_STATUS_NONE;
661        } else if (remaining == CANNOT_STAT_ERROR) {
662            return STORAGE_STATUS_FAIL;
663        }
664        return remaining < LOW_STORAGE_THRESHOLD
665                ? STORAGE_STATUS_LOW
666                : STORAGE_STATUS_OK;
667    }
668
669    private void readTimeLapseVideoPreferences() {
670        // Read CamcorderProfile quality.
671        String qualityStr = mPreferences.getString(
672                CameraSettings.KEY_VIDEO_TIME_LAPSE_QUALITY,
673                getString(R.string.pref_video_time_lapse_quality_default));
674        mProfile = CamcorderProfile.get(mCameraId, Integer.parseInt(qualityStr));
675
676        // Read interval between frame capture.
677        String frameIntervalStr = mPreferences.getString(
678                CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
679                getString(R.string.pref_video_time_lapse_frame_interval_default));
680        mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr);
681
682        mMaxVideoDurationInMs = 0; // No limit
683        getDesiredPreviewSize();
684    }
685
686    private void readVideoPreferences() {
687        if (mCaptureTimeLapse) {
688            readTimeLapseVideoPreferences();
689            return;
690        }
691
692        String quality = mPreferences.getString(
693                CameraSettings.KEY_VIDEO_QUALITY,
694                CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE);
695
696        boolean videoQualityHigh = CameraSettings.getVideoQuality(quality);
697
698        // Set video quality.
699        Intent intent = getIntent();
700        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
701            int extraVideoQuality =
702                    intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
703            videoQualityHigh = (extraVideoQuality > 0);
704        }
705
706        // Set video duration limit. The limit is read from the preference,
707        // unless it is specified in the intent.
708        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
709            int seconds =
710                    intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0);
711            mMaxVideoDurationInMs = 1000 * seconds;
712        } else {
713            mMaxVideoDurationInMs =
714                    CameraSettings.getVidoeDurationInMillis(quality);
715        }
716        mProfile = CamcorderProfile.get(mCameraId,
717                videoQualityHigh
718                ? CamcorderProfile.QUALITY_HIGH
719                : CamcorderProfile.QUALITY_LOW);
720        getDesiredPreviewSize();
721    }
722
723    private void getDesiredPreviewSize() {
724        mParameters = mCameraDevice.getParameters();
725        if (mParameters.getSupportedVideoSizes() == null) {
726            mDesiredPreviewWidth = mProfile.videoFrameWidth;
727            mDesiredPreviewHeight = mProfile.videoFrameHeight;
728        } else {  // Driver supports separates outputs for preview and video.
729            List<Size> sizes = mParameters.getSupportedPreviewSizes();
730            Size preferred = mParameters.getPreferredPreviewSizeForVideo();
731            int product = preferred.width * preferred.height;
732            Iterator it = sizes.iterator();
733            // Remove the preview sizes that are not preferred.
734            while (it.hasNext()) {
735                Size size = (Size) it.next();
736                if (size.width * size.height > product) {
737                    it.remove();
738                }
739            }
740            Size optimalSize = Util.getOptimalPreviewSize(this, sizes,
741                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
742            mDesiredPreviewWidth = optimalSize.width;
743            mDesiredPreviewHeight = optimalSize.height;
744        }
745        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
746                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
747    }
748
749    private void resizeForPreviewAspectRatio() {
750        mPreviewFrameLayout.setAspectRatio(
751                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
752    }
753
754    @Override
755    protected void onResume() {
756        super.onResume();
757        mPausing = false;
758
759        // Start orientation listener as soon as possible because it takes
760        // some time to get first orientation.
761        mOrientationListener.enable();
762        mVideoPreview.setVisibility(View.VISIBLE);
763        if (!mPreviewing && !mOpenCameraFail) {
764            if (!openCamera()) return;
765            readVideoPreferences();
766            resizeForPreviewAspectRatio();
767            startPreview();
768        }
769        keepScreenOnAwhile();
770
771        // install an intent filter to receive SD card related events.
772        IntentFilter intentFilter =
773                new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
774        intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
775        intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
776        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
777        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
778        intentFilter.addDataScheme("file");
779        mReceiver = new MyBroadcastReceiver();
780        registerReceiver(mReceiver, intentFilter);
781        mStorageStatus = getStorageStatus(true);
782
783        mHandler.postDelayed(new Runnable() {
784            public void run() {
785                showStorageHint();
786            }
787        }, 200);
788
789        changeHeadUpDisplayState();
790
791        // Update the last video thumbnail.
792        if (!mIsVideoCaptureIntent) {
793            if (mThumbnailButton != null && !mThumbnailButton.isUriValid()) {
794                updateThumbnailButton();
795            }
796        }
797    }
798
799    private void setPreviewDisplay(SurfaceHolder holder) {
800        try {
801            mCameraDevice.setPreviewDisplay(holder);
802        } catch (Throwable ex) {
803            closeCamera();
804            throw new RuntimeException("setPreviewDisplay failed", ex);
805        }
806    }
807
808    private boolean openCamera() {
809        try {
810            if (mCameraDevice == null) {
811                mCameraDevice = CameraHolder.instance().open(mCameraId);
812            }
813        } catch (CameraHardwareException e) {
814            showCameraErrorAndFinish();
815            return false;
816        }
817        return true;
818    }
819
820    private void startPreview() {
821        Log.v(TAG, "startPreview");
822        mCameraDevice.setErrorCallback(mErrorCallback);
823
824        if (mPreviewing == true) {
825            mCameraDevice.stopPreview();
826            mPreviewing = false;
827        }
828        setPreviewDisplay(mSurfaceHolder);
829        Util.setCameraDisplayOrientation(this, mCameraId, mCameraDevice);
830        setCameraParameters();
831
832        try {
833            mCameraDevice.startPreview();
834            mPreviewing = true;
835        } catch (Throwable ex) {
836            closeCamera();
837            throw new RuntimeException("startPreview failed", ex);
838        }
839    }
840
841    private void closeCamera() {
842        Log.v(TAG, "closeCamera");
843        if (mCameraDevice == null) {
844            Log.d(TAG, "already stopped.");
845            return;
846        }
847        // If we don't lock the camera, release() will fail.
848        mCameraDevice.lock();
849        CameraHolder.instance().release();
850        mCameraDevice = null;
851        mPreviewing = false;
852    }
853
854    private void finishRecorderAndCloseCamera() {
855        // This is similar to what mShutterButton.performClick() does,
856        // but not quite the same.
857        if (mMediaRecorderRecording) {
858            if (mIsVideoCaptureIntent) {
859                stopVideoRecording();
860                showAlert();
861            } else {
862                stopVideoRecording();
863                getThumbnail();
864            }
865        } else {
866            stopVideoRecording();
867        }
868        closeCamera();
869    }
870
871    @Override
872    protected void onPause() {
873        super.onPause();
874        mPausing = true;
875
876        changeHeadUpDisplayState();
877        if (mControlPanel != null) mControlPanel.dismissSettingPopup();
878
879        // Hide the preview now. Otherwise, the preview may be rotated during
880        // onPause and it is annoying to users.
881        mVideoPreview.setVisibility(View.INVISIBLE);
882
883        finishRecorderAndCloseCamera();
884
885        if (mReceiver != null) {
886            unregisterReceiver(mReceiver);
887            mReceiver = null;
888        }
889        resetScreenOn();
890
891        if (!mIsVideoCaptureIntent && mThumbnailButton != null) {
892            mThumbnailButton.storeData(ImageManager.getLastVideoThumbPath());
893        }
894
895        if (mStorageHint != null) {
896            mStorageHint.cancel();
897            mStorageHint = null;
898        }
899
900        mOrientationListener.disable();
901    }
902
903    @Override
904    public void onUserInteraction() {
905        super.onUserInteraction();
906        if (!mMediaRecorderRecording) keepScreenOnAwhile();
907    }
908
909    @Override
910    public void onBackPressed() {
911        if (mPausing) return;
912        if (mMediaRecorderRecording) {
913            onStopVideoRecording(false);
914        } else if (!collapseCameraControls()) {
915            super.onBackPressed();
916        }
917    }
918
919    @Override
920    public boolean onKeyDown(int keyCode, KeyEvent event) {
921        // Do not handle any key if the activity is paused.
922        if (mPausing) {
923            return true;
924        }
925
926        switch (keyCode) {
927            case KeyEvent.KEYCODE_CAMERA:
928                if (event.getRepeatCount() == 0) {
929                    mShutterButton.performClick();
930                    return true;
931                }
932                break;
933            case KeyEvent.KEYCODE_DPAD_CENTER:
934                if (event.getRepeatCount() == 0) {
935                    mShutterButton.performClick();
936                    return true;
937                }
938                break;
939            case KeyEvent.KEYCODE_MENU:
940                if (mMediaRecorderRecording) {
941                    onStopVideoRecording(true);
942                    return true;
943                }
944                break;
945        }
946
947        return super.onKeyDown(keyCode, event);
948    }
949
950    @Override
951    public boolean onKeyUp(int keyCode, KeyEvent event) {
952        switch (keyCode) {
953            case KeyEvent.KEYCODE_CAMERA:
954                mShutterButton.setPressed(false);
955                return true;
956        }
957        return super.onKeyUp(keyCode, event);
958    }
959
960    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
961        // Make sure we have a surface in the holder before proceeding.
962        if (holder.getSurface() == null) {
963            Log.d(TAG, "holder.getSurface() == null");
964            return;
965        }
966
967        mSurfaceHolder = holder;
968
969        if (mPausing) {
970            // We're pausing, the screen is off and we already stopped
971            // video recording. We don't want to start the camera again
972            // in this case in order to conserve power.
973            // The fact that surfaceChanged is called _after_ an onPause appears
974            // to be legitimate since in that case the lockscreen always returns
975            // to portrait orientation possibly triggering the notification.
976            return;
977        }
978
979        // The mCameraDevice will be null if it is fail to connect to the
980        // camera hardware. In this case we will show a dialog and then
981        // finish the activity, so it's OK to ignore it.
982        if (mCameraDevice == null) return;
983
984        // Set preview display if the surface is being created. Preview was
985        // already started.
986        if (holder.isCreating()) {
987            setPreviewDisplay(holder);
988        } else {
989            stopVideoRecording();
990            startPreview();
991        }
992    }
993
994    public void surfaceCreated(SurfaceHolder holder) {
995    }
996
997    public void surfaceDestroyed(SurfaceHolder holder) {
998        mSurfaceHolder = null;
999    }
1000
1001    private void gotoGallery() {
1002        MenuHelper.gotoCameraVideoGallery(this);
1003    }
1004
1005    @Override
1006    public boolean onCreateOptionsMenu(Menu menu) {
1007        super.onCreateOptionsMenu(menu);
1008
1009        if (mIsVideoCaptureIntent) {
1010            // No options menu for attach mode.
1011            return false;
1012        } else {
1013            addBaseMenuItems(menu);
1014        }
1015        return true;
1016    }
1017
1018    private boolean isVideoCaptureIntent() {
1019        String action = getIntent().getAction();
1020        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
1021    }
1022
1023    private void doReturnToCaller(boolean valid) {
1024        Intent resultIntent = new Intent();
1025        int resultCode;
1026        if (valid) {
1027            resultCode = RESULT_OK;
1028            resultIntent.setData(mCurrentVideoUri);
1029        } else {
1030            resultCode = RESULT_CANCELED;
1031        }
1032        setResult(resultCode, resultIntent);
1033        finish();
1034    }
1035
1036    /**
1037     * Returns
1038     *
1039     * @return number of bytes available, or an ERROR code.
1040     */
1041    private static long getAvailableStorage() {
1042        try {
1043            if (!ImageManager.hasStorage()) {
1044                return NO_STORAGE_ERROR;
1045            } else {
1046                String storageDirectory =
1047                        Environment.getExternalStorageDirectory().toString();
1048                StatFs stat = new StatFs(storageDirectory);
1049                return (long) stat.getAvailableBlocks()
1050                        * (long) stat.getBlockSize();
1051            }
1052        } catch (Exception ex) {
1053            // if we can't stat the filesystem then we don't know how many
1054            // free bytes exist. It might be zero but just leave it
1055            // blank since we really don't know.
1056            Log.e(TAG, "Fail to access sdcard", ex);
1057            return CANNOT_STAT_ERROR;
1058        }
1059    }
1060
1061    private void cleanupEmptyFile() {
1062        if (mVideoFilename != null) {
1063            File f = new File(mVideoFilename);
1064            if (f.length() == 0 && f.delete()) {
1065                Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
1066                mVideoFilename = null;
1067            }
1068        }
1069    }
1070
1071    // Prepares media recorder.
1072    private void initializeRecorder() {
1073        Log.v(TAG, "initializeRecorder");
1074        // If the mCameraDevice is null, then this activity is going to finish
1075        if (mCameraDevice == null) return;
1076
1077        if (mSurfaceHolder == null) {
1078            Log.v(TAG, "Surface holder is null. Wait for surface changed.");
1079            return;
1080        }
1081
1082        Intent intent = getIntent();
1083        Bundle myExtras = intent.getExtras();
1084
1085        long requestedSizeLimit = 0;
1086        if (mIsVideoCaptureIntent && myExtras != null) {
1087            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
1088            if (saveUri != null) {
1089                try {
1090                    mVideoFileDescriptor =
1091                            mContentResolver.openFileDescriptor(saveUri, "rw");
1092                    mCurrentVideoUri = saveUri;
1093                } catch (java.io.FileNotFoundException ex) {
1094                    // invalid uri
1095                    Log.e(TAG, ex.toString());
1096                }
1097            }
1098            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
1099        }
1100        mMediaRecorder = new MediaRecorder();
1101
1102        // Unlock the camera object before passing it to media recorder.
1103        mCameraDevice.unlock();
1104        mMediaRecorder.setCamera(mCameraDevice);
1105        if (!mCaptureTimeLapse) {
1106            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
1107        }
1108        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
1109        mMediaRecorder.setProfile(mProfile);
1110        if (mMaxVideoDurationInMs != 0) {
1111            mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
1112        }
1113        if (mCaptureTimeLapse) {
1114            mMediaRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
1115        }
1116
1117        // Set output file.
1118        if (mStorageStatus != STORAGE_STATUS_OK) {
1119            mMediaRecorder.setOutputFile("/dev/null");
1120        } else {
1121            // Try Uri in the intent first. If it doesn't exist, use our own
1122            // instead.
1123            if (mVideoFileDescriptor != null) {
1124                mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
1125                try {
1126                    mVideoFileDescriptor.close();
1127                } catch (IOException e) {
1128                    Log.e(TAG, "Fail to close fd", e);
1129                }
1130            } else {
1131                createVideoPath(mProfile.fileFormat);
1132                mMediaRecorder.setOutputFile(mVideoFilename);
1133            }
1134        }
1135
1136        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
1137
1138        // Set maximum file size.
1139        // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter
1140        // of that to make it more likely that recording can complete
1141        // successfully.
1142        long maxFileSize = getAvailableStorage() - LOW_STORAGE_THRESHOLD / 4;
1143        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
1144            maxFileSize = requestedSizeLimit;
1145        }
1146
1147        try {
1148            mMediaRecorder.setMaxFileSize(maxFileSize);
1149        } catch (RuntimeException exception) {
1150            // We are going to ignore failure of setMaxFileSize here, as
1151            // a) The composer selected may simply not support it, or
1152            // b) The underlying media framework may not handle 64-bit range
1153            // on the size restriction.
1154        }
1155
1156        // See android.hardware.Camera.Parameters.setRotation for
1157        // documentation.
1158        int rotation = 0;
1159        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
1160            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
1161            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
1162                rotation = (info.orientation - mOrientation + 360) % 360;
1163            } else {  // back-facing camera
1164                rotation = (info.orientation + mOrientation) % 360;
1165            }
1166        }
1167        mMediaRecorder.setOrientationHint(rotation);
1168        mOrientationHint = rotation;
1169
1170        try {
1171            mMediaRecorder.prepare();
1172        } catch (IOException e) {
1173            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
1174            releaseMediaRecorder();
1175            throw new RuntimeException(e);
1176        }
1177
1178        mMediaRecorder.setOnErrorListener(this);
1179        mMediaRecorder.setOnInfoListener(this);
1180    }
1181
1182    private void releaseMediaRecorder() {
1183        Log.v(TAG, "Releasing media recorder.");
1184        if (mMediaRecorder != null) {
1185            cleanupEmptyFile();
1186            mMediaRecorder.reset();
1187            mMediaRecorder.release();
1188            mMediaRecorder = null;
1189        }
1190        // Take back the camera object control from media recorder. Camera
1191        // device may be null if the activity is paused.
1192        if (mCameraDevice != null) mCameraDevice.lock();
1193    }
1194
1195    private void createVideoPath(int outputFileFormat) {
1196        long dateTaken = System.currentTimeMillis();
1197        String title = createName(dateTaken);
1198        String filename = title + ".3gp"; // Used when emailing.
1199        String mime = "video/3gpp";
1200        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
1201            filename = title + ".mp4";
1202            mime = "video/mp4";
1203        }
1204        String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME;
1205        String filePath = cameraDirPath + "/" + filename;
1206        File cameraDir = new File(cameraDirPath);
1207        cameraDir.mkdirs();
1208        ContentValues values = new ContentValues(7);
1209        values.put(Video.Media.TITLE, title);
1210        values.put(Video.Media.DISPLAY_NAME, filename);
1211        values.put(Video.Media.DATE_TAKEN, dateTaken);
1212        values.put(Video.Media.MIME_TYPE, mime);
1213        values.put(Video.Media.DATA, filePath);
1214        mVideoFilename = filePath;
1215        Log.v(TAG, "Current camera video filename: " + mVideoFilename);
1216        mCurrentVideoValues = values;
1217    }
1218
1219    private void registerVideo() {
1220        if (mVideoFileDescriptor == null) {
1221            Uri videoTable = Uri.parse("content://media/external/video/media");
1222            mCurrentVideoValues.put(Video.Media.SIZE,
1223                    new File(mCurrentVideoFilename).length());
1224            try {
1225                mCurrentVideoUri = mContentResolver.insert(videoTable,
1226                        mCurrentVideoValues);
1227            } catch (Exception e) {
1228                // We failed to insert into the database. This can happen if
1229                // the SD card is unmounted.
1230                mCurrentVideoUri = null;
1231                mCurrentVideoFilename = null;
1232            } finally {
1233                Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
1234            }
1235        }
1236        mCurrentVideoValues = null;
1237    }
1238
1239    private void deleteCurrentVideo() {
1240        if (mCurrentVideoFilename != null) {
1241            deleteVideoFile(mCurrentVideoFilename);
1242            mCurrentVideoFilename = null;
1243        }
1244        if (mCurrentVideoUri != null) {
1245            mContentResolver.delete(mCurrentVideoUri, null, null);
1246            mCurrentVideoUri = null;
1247        }
1248        updateAndShowStorageHint(true);
1249    }
1250
1251    private void deleteVideoFile(String fileName) {
1252        Log.v(TAG, "Deleting video " + fileName);
1253        File f = new File(fileName);
1254        if (!f.delete()) {
1255            Log.v(TAG, "Could not delete " + fileName);
1256        }
1257    }
1258
1259    private void setTimeLapseSwitchTitle(boolean enableTimeLapse) {
1260        int labelId = enableTimeLapse
1261                ? R.string.enable_time_lapse_mode
1262                : R.string.disable_time_lapse_mode;
1263
1264        mSwitchTimeLapseMenuItem.setTitle(labelId);
1265    }
1266
1267    private void addBaseMenuItems(Menu menu) {
1268        MenuHelper.addSwitchModeMenuItem(menu, false, new Runnable() {
1269            public void run() {
1270                switchToCameraMode();
1271            }
1272        });
1273        MenuItem gallery = menu.add(Menu.NONE, Menu.NONE,
1274                MenuHelper.POSITION_GOTO_GALLERY,
1275                R.string.camera_gallery_photos_text)
1276                .setOnMenuItemClickListener(
1277                    new OnMenuItemClickListener() {
1278                        public boolean onMenuItemClick(MenuItem item) {
1279                            gotoGallery();
1280                            return true;
1281                        }
1282                    });
1283        gallery.setIcon(android.R.drawable.ic_menu_gallery);
1284        mGalleryItems.add(gallery);
1285
1286        if (mNumberOfCameras > 1) {
1287            menu.add(Menu.NONE, Menu.NONE,
1288                    MenuHelper.POSITION_SWITCH_CAMERA_ID,
1289                    R.string.switch_camera_id)
1290                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
1291                public boolean onMenuItemClick(MenuItem item) {
1292                    CameraSettings.writePreferredCameraId(mPreferences,
1293                            ((mCameraId == mFrontCameraId)
1294                            ? mBackCameraId : mFrontCameraId));
1295                    onSharedPreferenceChanged();
1296                    return true;
1297                }
1298            }).setIcon(android.R.drawable.ic_menu_camera);
1299        }
1300
1301        mSwitchTimeLapseMenuItem = menu.add(Menu.NONE, Menu.NONE,
1302                MenuHelper.POSITION_SWITCH_TIME_LAPSE_MODE,
1303                R.string.enable_time_lapse_mode)
1304                .setOnMenuItemClickListener(new OnMenuItemClickListener() {
1305            public boolean onMenuItemClick(MenuItem item) {
1306                switchTimeLapseMode();
1307                return true;
1308            }
1309        }).setIcon(android.R.drawable.ic_menu_camera);
1310    }
1311
1312    private void switchTimeLapseMode() {
1313        mCaptureTimeLapse = !mCaptureTimeLapse;
1314
1315        // Read the video preferences
1316        mCameraDevice.stopPreview();
1317        readVideoPreferences();
1318        resizeForPreviewAspectRatio();
1319        startPreview();
1320
1321        // Reload the UI.
1322        initializeHeadUpDisplay();
1323        initializeControlPanel();
1324
1325        // Change menu
1326        setTimeLapseSwitchTitle(!mCaptureTimeLapse);
1327    }
1328
1329    private void switchCameraId(int cameraId) {
1330        if (mPausing) return;
1331        mCameraId = cameraId;
1332
1333        finishRecorderAndCloseCamera();
1334
1335        // Reload the preferences.
1336        mPreferences.setLocalId(this, mCameraId);
1337        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
1338        // Read media profile again because camera id is changed.
1339        openCamera();
1340        readVideoPreferences();
1341        resizeForPreviewAspectRatio();
1342        startPreview();
1343
1344        // Reload the UI.
1345        initializeHeadUpDisplay();
1346        initializeControlPanel();
1347    }
1348
1349    private PreferenceGroup filterPreferenceScreenByIntent(
1350            PreferenceGroup screen) {
1351        Intent intent = getIntent();
1352        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
1353            CameraSettings.removePreferenceFromScreen(screen,
1354                    CameraSettings.KEY_VIDEO_QUALITY);
1355        }
1356
1357        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
1358            CameraSettings.removePreferenceFromScreen(screen,
1359                    CameraSettings.KEY_VIDEO_QUALITY);
1360        }
1361        return screen;
1362    }
1363
1364    // from MediaRecorder.OnErrorListener
1365    public void onError(MediaRecorder mr, int what, int extra) {
1366        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
1367            // We may have run out of space on the sdcard.
1368            stopVideoRecording();
1369            updateAndShowStorageHint(true);
1370        }
1371    }
1372
1373    // from MediaRecorder.OnInfoListener
1374    public void onInfo(MediaRecorder mr, int what, int extra) {
1375        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
1376            if (mMediaRecorderRecording) onStopVideoRecording(true);
1377        } else if (what
1378                == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
1379            if (mMediaRecorderRecording) onStopVideoRecording(true);
1380
1381            // Show the toast.
1382            Toast.makeText(VideoCamera.this, R.string.video_reach_size_limit,
1383                           Toast.LENGTH_LONG).show();
1384        }
1385    }
1386
1387    /*
1388     * Make sure we're not recording music playing in the background, ask the
1389     * MediaPlaybackService to pause playback.
1390     */
1391    private void pauseAudioPlayback() {
1392        // Shamelessly copied from MediaPlaybackService.java, which
1393        // should be public, but isn't.
1394        Intent i = new Intent("com.android.music.musicservicecommand");
1395        i.putExtra("command", "pause");
1396
1397        sendBroadcast(i);
1398    }
1399
1400    private void startVideoRecording() {
1401        Log.v(TAG, "startVideoRecording");
1402        if (mStorageStatus != STORAGE_STATUS_OK) {
1403            Log.v(TAG, "Storage issue, ignore the start request");
1404            return;
1405        }
1406
1407        initializeRecorder();
1408        if (mMediaRecorder == null) {
1409            Log.e(TAG, "Fail to initialize media recorder");
1410            return;
1411        }
1412
1413        pauseAudioPlayback();
1414
1415        try {
1416            mMediaRecorder.start(); // Recording is now started
1417        } catch (RuntimeException e) {
1418            Log.e(TAG, "Could not start media recorder. ", e);
1419            releaseMediaRecorder();
1420            return;
1421        }
1422        enableCameraControls(false);
1423
1424        mMediaRecorderRecording = true;
1425        mRecordingStartTime = SystemClock.uptimeMillis();
1426        updateRecordingIndicator(false);
1427        mRecordingTimeView.setText("");
1428        mRecordingTimeView.setVisibility(View.VISIBLE);
1429        mCameraPicker.setVisibility(View.GONE);
1430        if (mTimeLapseRecordingTimeView != null) {
1431            mTimeLapseRecordingTimeView.setText("");
1432            mTimeLapseRecordingTimeView.setVisibility(View.VISIBLE);
1433        }
1434
1435        updateRecordingTime();
1436        keepScreenOn();
1437    }
1438
1439    private void updateRecordingIndicator(boolean showRecording) {
1440        int drawableId =
1441                showRecording ? R.drawable.btn_ic_video_record
1442                        : R.drawable.btn_ic_video_record_stop;
1443        Drawable drawable = getResources().getDrawable(drawableId);
1444        mShutterButton.setImageDrawable(drawable);
1445    }
1446
1447    private void getThumbnail() {
1448        acquireVideoThumb();
1449    }
1450
1451    private void showAlert() {
1452        fadeOut(findViewById(R.id.shutter_button));
1453        if (mCurrentVideoFilename != null) {
1454            Bitmap src = ThumbnailUtils.createVideoThumbnail(
1455                    mCurrentVideoFilename, Video.Thumbnails.MINI_KIND);
1456            // MetadataRetriever already rotates the thumbnail. We should rotate
1457            // it back (and mirror if it is front-facing camera).
1458            CameraInfo[] info = CameraHolder.instance().getCameraInfo();
1459            if (info[mCameraId].facing == CameraInfo.CAMERA_FACING_BACK) {
1460                src = Util.rotateAndMirror(src, -mOrientationHint, false);
1461            } else {
1462                src = Util.rotateAndMirror(src, -mOrientationHint, true);
1463            }
1464            mVideoFrame.setImageBitmap(src);
1465            mVideoFrame.setVisibility(View.VISIBLE);
1466        }
1467        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1468        for (int id : pickIds) {
1469            View button = findViewById(id);
1470            fadeIn(((View) button.getParent()));
1471        }
1472    }
1473
1474    private void hideAlert() {
1475        mVideoFrame.setVisibility(View.INVISIBLE);
1476        fadeIn(findViewById(R.id.shutter_button));
1477        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1478        for (int id : pickIds) {
1479            View button = findViewById(id);
1480            fadeOut(((View) button.getParent()));
1481        }
1482    }
1483
1484    private static void fadeIn(View view) {
1485        view.setVisibility(View.VISIBLE);
1486        Animation animation = new AlphaAnimation(0F, 1F);
1487        animation.setDuration(500);
1488        view.startAnimation(animation);
1489    }
1490
1491    private static void fadeOut(View view) {
1492        view.setVisibility(View.INVISIBLE);
1493        Animation animation = new AlphaAnimation(1F, 0F);
1494        animation.setDuration(500);
1495        view.startAnimation(animation);
1496    }
1497
1498    private boolean isAlertVisible() {
1499        return this.mVideoFrame.getVisibility() == View.VISIBLE;
1500    }
1501
1502    private void viewVideo(RotateImageView view) {
1503        if(view.isUriValid()) {
1504            Intent intent = new Intent(Util.REVIEW_ACTION, view.getUri());
1505            try {
1506                startActivity(intent);
1507            } catch (ActivityNotFoundException ex) {
1508                try {
1509                    intent = new Intent(Intent.ACTION_VIEW, view.getUri());
1510                    startActivity(intent);
1511                } catch (ActivityNotFoundException e) {
1512                    Log.e(TAG, "review video fail. uri=" + view.getUri(), e);
1513                }
1514            }
1515        } else {
1516            Log.e(TAG, "Uri invalid. uri=" + view.getUri());
1517        }
1518    }
1519
1520    private void stopVideoRecording() {
1521        Log.v(TAG, "stopVideoRecording");
1522        if (mMediaRecorderRecording) {
1523            boolean needToRegisterRecording = false;
1524            mMediaRecorder.setOnErrorListener(null);
1525            mMediaRecorder.setOnInfoListener(null);
1526            try {
1527                mMediaRecorder.stop();
1528                mCurrentVideoFilename = mVideoFilename;
1529                Log.v(TAG, "Setting current video filename: "
1530                        + mCurrentVideoFilename);
1531                needToRegisterRecording = true;
1532            } catch (RuntimeException e) {
1533                Log.e(TAG, "stop fail: " + e.getMessage());
1534                deleteVideoFile(mVideoFilename);
1535            }
1536            mMediaRecorderRecording = false;
1537            enableCameraControls(true);
1538            updateRecordingIndicator(true);
1539            mRecordingTimeView.setVisibility(View.GONE);
1540            mCameraPicker.setVisibility(View.VISIBLE);
1541            if (mTimeLapseRecordingTimeView != null) {
1542                mTimeLapseRecordingTimeView.setVisibility(View.GONE);
1543            }
1544            keepScreenOnAwhile();
1545            if (needToRegisterRecording && mStorageStatus == STORAGE_STATUS_OK) {
1546                registerVideo();
1547            }
1548            mVideoFilename = null;
1549            mVideoFileDescriptor = null;
1550        }
1551        releaseMediaRecorder();  // always release media recorder
1552    }
1553
1554    private void resetScreenOn() {
1555        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1556        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1557    }
1558
1559    private void keepScreenOnAwhile() {
1560        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1561        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1562        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1563    }
1564
1565    private void keepScreenOn() {
1566        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1567        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1568    }
1569
1570    private void acquireVideoThumb() {
1571        if (mThumbnailButton != null) {
1572            Bitmap videoFrame = ThumbnailUtils.createVideoThumbnail(
1573                    mCurrentVideoFilename, Video.Thumbnails.MINI_KIND);
1574            mThumbnailButton.setData(mCurrentVideoUri, videoFrame);
1575        }
1576    }
1577
1578    private void initThumbnailButton() {
1579        mThumbnailButton = (RotateImageView)findViewById(R.id.review_thumbnail);
1580        if (mThumbnailButton != null) {
1581            mThumbnailButton.setVisibility(View.VISIBLE);
1582            mThumbnailButton.setOnClickListener(this);
1583            mThumbnailButton.loadData(ImageManager.getLastVideoThumbPath());
1584        }
1585    }
1586
1587    private void updateThumbnailButton() {
1588        IImageList list = ImageManager.makeImageList(
1589                        mContentResolver,
1590                        dataLocation(),
1591                        ImageManager.INCLUDE_VIDEOS,
1592                        ImageManager.SORT_ASCENDING,
1593                        ImageManager.CAMERA_IMAGE_BUCKET_ID);
1594        int count = list.getCount();
1595        if (count > 0) {
1596            IImage image = list.getImageAt(count - 1);
1597            Uri uri = image.fullSizeImageUri();
1598            mThumbnailButton.setData(uri, image.miniThumbBitmap());
1599        } else {
1600            mThumbnailButton.setData(null, null);
1601        }
1602        list.close();
1603    }
1604
1605    private static ImageManager.DataLocation dataLocation() {
1606        return ImageManager.DataLocation.EXTERNAL;
1607    }
1608
1609    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
1610        long seconds = milliSeconds / 1000; // round down to compute seconds
1611        long minutes = seconds / 60;
1612        long hours = minutes / 60;
1613        long remainderMinutes = minutes - (hours * 60);
1614        long remainderSeconds = seconds - (minutes * 60);
1615
1616        StringBuilder timeStringBuilder = new StringBuilder();
1617
1618        // Hours
1619        if (hours > 0) {
1620            if (hours < 10) {
1621                timeStringBuilder.append('0');
1622            }
1623            timeStringBuilder.append(hours);
1624
1625            timeStringBuilder.append(':');
1626        }
1627
1628        // Minutes
1629        if (remainderMinutes < 10) {
1630            timeStringBuilder.append('0');
1631        }
1632        timeStringBuilder.append(remainderMinutes);
1633        timeStringBuilder.append(':');
1634
1635        // Seconds
1636        if (remainderSeconds < 10) {
1637            timeStringBuilder.append('0');
1638        }
1639        timeStringBuilder.append(remainderSeconds);
1640
1641        // Centi seconds
1642        if (displayCentiSeconds) {
1643            timeStringBuilder.append('.');
1644            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
1645            if (remainderCentiSeconds < 10) {
1646                timeStringBuilder.append('0');
1647            }
1648            timeStringBuilder.append(remainderCentiSeconds);
1649        }
1650
1651        return timeStringBuilder.toString();
1652    }
1653
1654    // Calculates the time lapse video length till now and returns it in
1655    // the format hh:mm:ss.dd, where dd are the centi seconds.
1656    private String getTimeLapseVideoLengthString(long deltaMs) {
1657        // For better approximation calculate fractional number of frames captured.
1658        // This will update the video time at a higher resolution.
1659        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
1660        long videoTimeMs =
1661            (long) (numberOfFrames / (double) mProfile.videoFrameRate * 1000);
1662        return millisecondToTimeString(videoTimeMs, true);
1663    }
1664
1665    private void updateRecordingTime() {
1666        if (!mMediaRecorderRecording) {
1667            return;
1668        }
1669        long now = SystemClock.uptimeMillis();
1670        long delta = now - mRecordingStartTime;
1671
1672        // Starting a minute before reaching the max duration
1673        // limit, we'll countdown the remaining time instead.
1674        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
1675                && delta >= mMaxVideoDurationInMs - 60000);
1676
1677        long deltaAdjusted = delta;
1678        if (countdownRemainingTime) {
1679            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
1680        }
1681        String text = millisecondToTimeString(deltaAdjusted, false);
1682
1683        if (mCaptureTimeLapse) {
1684            // Since the length of time lapse video is different from the length
1685            // of the actual wall clock time elapsed, we display the video length
1686            // alongside the wall clock time.
1687            String timeLapseText = "(" + getTimeLapseVideoLengthString(delta) + ")";
1688            // In xlarge layout, recording time and time lapse recording time
1689            // are separated in two lines. In other layouts, they are in one
1690            // line.
1691            if (mTimeLapseRecordingTimeView == null) {
1692                text += " " + timeLapseText;
1693            } else {
1694                mTimeLapseRecordingTimeView.setText(timeLapseText);
1695            }
1696        }
1697
1698        mRecordingTimeView.setText(text);
1699
1700        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1701            // Avoid setting the color on every update, do it only
1702            // when it needs changing.
1703            mRecordingTimeCountsDown = countdownRemainingTime;
1704
1705            int color = getResources().getColor(countdownRemainingTime
1706                    ? R.color.recording_time_remaining_text
1707                    : R.color.recording_time_elapsed_text);
1708
1709            mRecordingTimeView.setTextColor(color);
1710        }
1711
1712        long nextUpdateDelay = 1000 - (delta % 1000);
1713        mHandler.sendEmptyMessageDelayed(
1714                UPDATE_RECORD_TIME, nextUpdateDelay);
1715    }
1716
1717    private static boolean isSupported(String value, List<String> supported) {
1718        return supported == null ? false : supported.indexOf(value) >= 0;
1719    }
1720
1721    private void setCameraParameters() {
1722        mParameters = mCameraDevice.getParameters();
1723
1724        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
1725        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
1726
1727        // Set flash mode.
1728        String flashMode = mPreferences.getString(
1729                CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
1730                getString(R.string.pref_camera_video_flashmode_default));
1731        List<String> supportedFlash = mParameters.getSupportedFlashModes();
1732        if (isSupported(flashMode, supportedFlash)) {
1733            mParameters.setFlashMode(flashMode);
1734        } else {
1735            flashMode = mParameters.getFlashMode();
1736            if (flashMode == null) {
1737                flashMode = getString(
1738                        R.string.pref_camera_flashmode_no_flash);
1739            }
1740        }
1741
1742        // Set white balance parameter.
1743        String whiteBalance = mPreferences.getString(
1744                CameraSettings.KEY_WHITE_BALANCE,
1745                getString(R.string.pref_camera_whitebalance_default));
1746        if (isSupported(whiteBalance,
1747                mParameters.getSupportedWhiteBalance())) {
1748            mParameters.setWhiteBalance(whiteBalance);
1749        } else {
1750            whiteBalance = mParameters.getWhiteBalance();
1751            if (whiteBalance == null) {
1752                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
1753            }
1754        }
1755
1756        // Set color effect parameter.
1757        String colorEffect = mPreferences.getString(
1758                CameraSettings.KEY_COLOR_EFFECT,
1759                getString(R.string.pref_camera_coloreffect_default));
1760        if (isSupported(colorEffect, mParameters.getSupportedColorEffects())) {
1761            mParameters.setColorEffect(colorEffect);
1762        }
1763
1764        mCameraDevice.setParameters(mParameters);
1765        // Keep preview size up to date.
1766        mParameters = mCameraDevice.getParameters();
1767    }
1768
1769    private boolean switchToCameraMode() {
1770        if (isFinishing() || mMediaRecorderRecording) return false;
1771        MenuHelper.gotoCameraMode(VideoCamera.this);
1772        finish();
1773        return true;
1774    }
1775
1776    public boolean onSwitchChanged(Switcher source, boolean onOff) {
1777        if (onOff == SWITCH_CAMERA) {
1778            return switchToCameraMode();
1779        } else {
1780            return true;
1781        }
1782    }
1783
1784    @Override
1785    public void onConfigurationChanged(Configuration config) {
1786        super.onConfigurationChanged(config);
1787
1788        // If the camera resumes behind the lock screen, the orientation
1789        // will be portrait. That causes OOM when we try to allocation GPU
1790        // memory for the GLSurfaceView again when the orientation changes. So,
1791        // we delayed initialization of HeadUpDisplay until the orientation
1792        // becomes landscape.
1793        changeHeadUpDisplayState();
1794    }
1795
1796    public void onSizeChanged() {
1797        // TODO: update the content on GLRootView
1798    }
1799
1800    private class MyHeadUpDisplayListener implements HeadUpDisplay.Listener {
1801        public void onSharedPreferenceChanged() {
1802            mHandler.post(new Runnable() {
1803                public void run() {
1804                    VideoCamera.this.onSharedPreferenceChanged();
1805                }
1806            });
1807        }
1808
1809        public void onRestorePreferencesClicked() {
1810            mHandler.post(new Runnable() {
1811                public void run() {
1812                    VideoCamera.this.onRestorePreferencesClicked();
1813                }
1814            });
1815        }
1816
1817        public void onPopupWindowVisibilityChanged(final int visibility) {
1818        }
1819    }
1820
1821    private void onRestorePreferencesClicked() {
1822        Runnable runnable = new Runnable() {
1823            public void run() {
1824                if (mHeadUpDisplay != null) {
1825                    mHeadUpDisplay.restorePreferences(mParameters);
1826                }
1827            }
1828        };
1829        MenuHelper.confirmAction(this,
1830                getString(R.string.confirm_restore_title),
1831                getString(R.string.confirm_restore_message),
1832                runnable);
1833    }
1834
1835    private void onSharedPreferenceChanged() {
1836        // ignore the events after "onPause()" or preview has not started yet
1837        if (mPausing) return;
1838        synchronized (mPreferences) {
1839            // If mCameraDevice is not ready then we can set the parameter in
1840            // startPreview().
1841            if (mCameraDevice == null) return;
1842
1843            // Check if camera id is changed.
1844            int cameraId = CameraSettings.readPreferredCameraId(mPreferences);
1845            if (mCameraId != cameraId) {
1846                switchCameraId(cameraId);
1847            } else {
1848                readVideoPreferences();
1849                // We need to restart the preview if preview size is changed.
1850                Size size = mParameters.getPreviewSize();
1851                if (size.width != mDesiredPreviewWidth
1852                        || size.height != mDesiredPreviewHeight) {
1853                    mCameraDevice.stopPreview();
1854                    resizeForPreviewAspectRatio();
1855                    startPreview(); // Parameters will be set in startPreview().
1856                } else {
1857                    setCameraParameters();
1858                }
1859            }
1860        }
1861    }
1862
1863    private class MyControlPanelListener implements ControlPanel.Listener {
1864        public void onSharedPreferenceChanged() {
1865            VideoCamera.this.onSharedPreferenceChanged();
1866        }
1867    }
1868
1869    private class MyCameraPickerListener implements CameraPicker.Listener {
1870        public void onSharedPreferenceChanged() {
1871            VideoCamera.this.onSharedPreferenceChanged();
1872        }
1873    }
1874
1875    @Override
1876    public boolean dispatchTouchEvent(MotionEvent m) {
1877        // Check if the popup window should be dismissed first.
1878        if (mPopupGestureDetector != null && mPopupGestureDetector.onTouchEvent(m)) {
1879            return true;
1880        }
1881
1882        return super.dispatchTouchEvent(m);
1883    }
1884
1885    private int mPopupLocations[] = new int[2];
1886    private class PopupGestureListener extends
1887            GestureDetector.SimpleOnGestureListener {
1888        public boolean onDown(MotionEvent e) {
1889            // Check if the popup window is visible.
1890            View v = mControlPanel.getActivePopupWindow();
1891            if (v == null) return false;
1892
1893            int x = Math.round(e.getX());
1894            int y = Math.round(e.getY());
1895
1896            // Dismiss the popup window if users touch on the outside.
1897            v.getLocationOnScreen(mPopupLocations);
1898            if (x < mPopupLocations[0] || x > mPopupLocations[0] + v.getWidth()
1899                    || y < mPopupLocations[1] || y > mPopupLocations[1] + v.getHeight()) {
1900                mControlPanel.dismissSettingPopup();
1901                // Let event fall through.
1902            }
1903            return false;
1904        }
1905    }
1906}
1907