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