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