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