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