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