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