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