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