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