VideoCamera.java revision 61b98317dfb7b8bad4e826e1db560128e1c37374
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;
21
22import android.app.Activity;
23import android.content.ActivityNotFoundException;
24import android.content.BroadcastReceiver;
25import android.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.SharedPreferences;
31import android.graphics.Bitmap;
32import android.graphics.drawable.Drawable;
33import android.media.MediaRecorder;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Environment;
37import android.os.Handler;
38import android.os.Message;
39import android.os.StatFs;
40import android.os.SystemClock;
41import android.preference.PreferenceManager;
42import android.provider.MediaStore;
43import android.provider.MediaStore.Video;
44import android.text.format.DateFormat;
45import android.util.Log;
46import android.view.KeyEvent;
47import android.view.LayoutInflater;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.SurfaceHolder;
51import android.view.View;
52import android.view.ViewGroup;
53import android.view.Window;
54import android.view.WindowManager;
55import android.view.MenuItem.OnMenuItemClickListener;
56import android.view.animation.AlphaAnimation;
57import android.view.animation.Animation;
58import android.widget.ImageView;
59import android.widget.TextView;
60import android.widget.Toast;
61
62import java.io.File;
63import java.io.FileDescriptor;
64import java.io.IOException;
65import java.text.SimpleDateFormat;
66import java.util.ArrayList;
67import java.util.Date;
68
69/**
70 * The Camcorder activity.
71 */
72public class VideoCamera extends Activity implements View.OnClickListener,
73        ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback,
74        MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener {
75
76    private static final String TAG = "videocamera";
77
78    private static final int INIT_RECORDER = 3;
79    private static final int CLEAR_SCREEN_DELAY = 4;
80    private static final int UPDATE_RECORD_TIME = 5;
81
82    private static final int SCREEN_DELAY = 2 * 60 * 1000;
83
84    private static final long NO_STORAGE_ERROR = -1L;
85    private static final long CANNOT_STAT_ERROR = -2L;
86    private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L;
87
88    private static final int STORAGE_STATUS_OK = 0;
89    private static final int STORAGE_STATUS_LOW = 1;
90    private static final int STORAGE_STATUS_NONE = 2;
91
92    public static final int MENU_SETTINGS = 6;
93    public static final int MENU_GALLERY_PHOTOS = 7;
94    public static final int MENU_GALLERY_VIDEOS = 8;
95    public static final int MENU_SAVE_GALLERY_PHOTO = 34;
96    public static final int MENU_SAVE_PLAY_VIDEO = 35;
97    public static final int MENU_SAVE_SELECT_VIDEO = 36;
98    public static final int MENU_SAVE_NEW_VIDEO = 37;
99
100    SharedPreferences mPreferences;
101
102    private static final float VIDEO_ASPECT_RATIO = 176.0f / 144.0f;
103    VideoPreview mVideoPreview;
104    SurfaceHolder mSurfaceHolder = null;
105    ImageView mVideoFrame;
106
107    private boolean mIsVideoCaptureIntent;
108    // mLastPictureButton and mThumbController
109    // are non-null only if mIsVideoCaptureIntent is true.
110    private ImageView mLastPictureButton;
111    private ThumbnailController mThumbController;
112
113    private int mStorageStatus = STORAGE_STATUS_OK;
114
115    private MediaRecorder mMediaRecorder;
116    private boolean mMediaRecorderRecording = false;
117    private long mRecordingStartTime;
118    // The video file that the hardware camera is about to record into
119    // (or is recording into.)
120    private String mCameraVideoFilename;
121    private FileDescriptor mCameraVideoFileDescriptor;
122
123    // The video file that has already been recorded, and that is being
124    // examined by the user.
125    private String mCurrentVideoFilename;
126    private Uri mCurrentVideoUri;
127    private ContentValues mCurrentVideoValues;
128
129    // The video frame size we will record (like 352x288).
130    private int mVideoWidth, mVideoHeight;
131
132    // The video duration limit.
133    private int mMaxVideoDurationInMs;
134
135    boolean mPausing = false;
136    boolean mPreviewing = false; // True if preview is started.
137    boolean mRecorderInitialized = false;
138
139    private ContentResolver mContentResolver;
140
141    private ShutterButton mShutterButton;
142    private TextView mRecordingTimeView;
143    private boolean mRecordingTimeCountsDown = false;
144
145    ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
146
147    private final Handler mHandler = new MainHandler();
148
149    // This Handler is used to post message back onto the main thread of the
150    // application
151    private class MainHandler extends Handler {
152        @Override
153        public void handleMessage(Message msg) {
154            switch (msg.what) {
155
156                case CLEAR_SCREEN_DELAY: {
157                    clearScreenOnFlag();
158                    break;
159                }
160
161                case UPDATE_RECORD_TIME: {
162                    updateRecordingTime();
163                    break;
164                }
165
166                case INIT_RECORDER: {
167                    initializeRecorder();
168                    break;
169                }
170
171                default:
172                    Log.v(TAG, "Unhandled message: " + msg.what);
173                    break;
174            }
175        }
176    }
177
178    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
179        @Override
180        public void onReceive(Context context, Intent intent) {
181            String action = intent.getAction();
182            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
183                updateAndShowStorageHint(false);
184                stopVideoRecording();
185            } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
186                updateAndShowStorageHint(true);
187                mRecorderInitialized = false;
188                initializeRecorder();
189            } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
190                // SD card unavailable
191                // handled in ACTION_MEDIA_EJECT
192            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
193                Toast.makeText(VideoCamera.this, getResources().getString(R.string.wait), 5000);
194            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
195                updateAndShowStorageHint(true);
196            }
197        }
198    };
199
200    private static String createName(long dateTaken) {
201        return DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString();
202    }
203
204    /** Called with the activity is first created. */
205    @Override
206    public void onCreate(Bundle icicle) {
207        super.onCreate(icicle);
208
209        /*
210         * To reduce startup time, we open camera device in another thread.
211         * Camera is opened in onCreate instead of onResume because there are
212         * lots of things to do here and camera open can be done in parallel. We
213         * will make sure the camera is opened at the end of onCreate.
214         */
215        Thread openCameraThread = new Thread(new Runnable() {
216            public void run() {
217                mCameraDevice = CameraHolder.instance().open();
218            }
219        });
220        openCameraThread.start();
221
222        mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
223        mContentResolver = getContentResolver();
224
225        requestWindowFeature(Window.FEATURE_PROGRESS);
226        setContentView(R.layout.video_camera);
227
228        mVideoPreview = (VideoPreview) findViewById(R.id.camera_preview);
229        mVideoPreview.setAspectRatio(VIDEO_ASPECT_RATIO);
230
231        // don't set mSurfaceHolder here. We have it set ONLY within
232        // surfaceCreated / surfaceDestroyed, other parts of the code
233        // assume that when it is set, the surface is also set.
234        SurfaceHolder holder = mVideoPreview.getHolder();
235        holder.addCallback(this);
236        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
237
238        mIsVideoCaptureIntent = isVideoCaptureIntent();
239        mRecordingTimeView = (TextView) findViewById(R.id.recording_time);
240        mVideoFrame = (ImageView) findViewById(R.id.video_frame);
241
242        ViewGroup rootView = (ViewGroup) findViewById(R.id.video_camera);
243        LayoutInflater inflater = this.getLayoutInflater();
244        if (!mIsVideoCaptureIntent) {
245            View controlBar = inflater.inflate(R.layout.camera_control, rootView);
246            mLastPictureButton = (ImageView) controlBar.findViewById(R.id.review_thumbnail);
247            mThumbController = new ThumbnailController(mLastPictureButton, mContentResolver);
248            mLastPictureButton.setOnClickListener(this);
249            mThumbController.loadData(ImageManager.getLastVideoThumbPath());
250            findViewById(R.id.camera_switch).setOnClickListener(this);
251        } else {
252            View controlBar = inflater.inflate(R.layout.attach_camera_control, rootView);
253            controlBar.findViewById(R.id.btn_cancel).setOnClickListener(this);
254            controlBar.findViewById(R.id.btn_retake).setOnClickListener(this);
255            controlBar.findViewById(R.id.btn_play).setOnClickListener(this);
256            controlBar.findViewById(R.id.btn_done).setOnClickListener(this);
257        }
258
259        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
260        mShutterButton.setImageResource(R.drawable.btn_ic_video_record);
261        mShutterButton.setOnShutterButtonListener(this);
262        mShutterButton.requestFocus();
263
264        // Make sure the camera is opened.
265        try {
266            openCameraThread.join();
267        } catch (InterruptedException ex) {
268            // ignore
269        }
270    }
271
272    private void startShareVideoActivity() {
273        Intent intent = new Intent();
274        intent.setAction(Intent.ACTION_SEND);
275        intent.setType("video/3gpp");
276        intent.putExtra(Intent.EXTRA_STREAM, mCurrentVideoUri);
277        try {
278            startActivity(Intent.createChooser(intent, getText(R.string.sendVideo)));
279        } catch (android.content.ActivityNotFoundException ex) {
280            Toast.makeText(VideoCamera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT)
281                    .show();
282        }
283    }
284
285    private void startPlayVideoActivity() {
286        Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri);
287        try {
288            startActivity(intent);
289        } catch (android.content.ActivityNotFoundException ex) {
290            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
291        }
292    }
293
294
295    public void onClick(View v) {
296        switch (v.getId()) {
297            case R.id.btn_retake:
298                discardCurrentVideoAndInitRecorder();
299                break;
300            case R.id.camera_switch:
301                MenuHelper.gotoCameraMode(this);
302                break;
303            case R.id.btn_play:
304                startPlayVideoActivity();
305                break;
306            case R.id.btn_done:
307                doReturnToCaller(true);
308                break;
309            case R.id.btn_cancel:
310                stopVideoRecording();
311                doReturnToCaller(false);
312                break;
313            case R.id.discard: {
314                Runnable deleteCallback = new Runnable() {
315                    public void run() {
316                        discardCurrentVideoAndInitRecorder();
317                    }
318                };
319                MenuHelper.deleteVideo(this, deleteCallback);
320                break;
321            }
322            case R.id.share: {
323                startShareVideoActivity();
324                break;
325            }
326            case R.id.play: {
327                doPlayCurrentVideo();
328                break;
329            }
330            case R.id.review_thumbnail: {
331                stopVideoRecordingAndShowReview();
332                break;
333            }
334        }
335    }
336
337    public void onShutterButtonFocus(ShutterButton button, boolean pressed) {
338        // Do nothing (everything happens in onShutterButtonClick).
339    }
340
341    public void onShutterButtonClick(ShutterButton button) {
342        switch (button.getId()) {
343            case R.id.shutter_button:
344                if (mMediaRecorderRecording) {
345                    if (mIsVideoCaptureIntent) {
346                        stopVideoRecordingAndShowAlert();
347                    } else {
348                        stopVideoRecordingAndGetThumbnail();
349                        mRecorderInitialized = false;
350                        initializeRecorder();
351                    }
352                } else if (mRecorderInitialized) {
353                    // If the click comes before recorder initialization, it is
354                    // ignored. If users click the button during initialization,
355                    // the event is put in the queue and record will be started
356                    // eventually.
357                    startVideoRecording();
358                }
359                break;
360        }
361    }
362
363    private void doPlayCurrentVideo() {
364        Log.v(TAG, "Playing current video: " + mCurrentVideoUri);
365        Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri);
366        try {
367            startActivity(intent);
368        } catch (android.content.ActivityNotFoundException ex) {
369            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
370        }
371    }
372
373    private void discardCurrentVideoAndInitRecorder() {
374        deleteCurrentVideo();
375        hideAlertAndInitializeRecorder();
376    }
377
378    private OnScreenHint mStorageHint;
379
380    private void updateAndShowStorageHint(boolean mayHaveSd) {
381        mStorageStatus = getStorageStatus(mayHaveSd);
382        showStorageHint();
383    }
384
385    private void showStorageHint() {
386        String errorMessage = null;
387        switch (mStorageStatus) {
388            case STORAGE_STATUS_NONE:
389                errorMessage = getString(R.string.no_storage);
390                break;
391            case STORAGE_STATUS_LOW:
392                errorMessage = getString(R.string.spaceIsLow_content);
393        }
394        if (errorMessage != null) {
395            if (mStorageHint == null) {
396                mStorageHint = OnScreenHint.makeText(this, errorMessage);
397            } else {
398                mStorageHint.setText(errorMessage);
399            }
400            mStorageHint.show();
401        } else if (mStorageHint != null) {
402            mStorageHint.cancel();
403            mStorageHint = null;
404        }
405    }
406
407    private int getStorageStatus(boolean mayHaveSd) {
408        long remaining = mayHaveSd ? getAvailableStorage() : NO_STORAGE_ERROR;
409        if (remaining == NO_STORAGE_ERROR) {
410            return STORAGE_STATUS_NONE;
411        }
412        return remaining < LOW_STORAGE_THRESHOLD ? STORAGE_STATUS_LOW : STORAGE_STATUS_OK;
413    }
414
415    private void readVideoSizePreference() {
416        boolean videoQualityHigh =
417                getBooleanPreference(CameraSettings.KEY_VIDEO_QUALITY,
418                        CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE);
419
420        // 1 minute = 60000ms
421        mMaxVideoDurationInMs =
422                60000 * getIntPreference(CameraSettings.KEY_VIDEO_DURATION,
423                        CameraSettings.DEFAULT_VIDEO_DURATION_VALUE);
424
425        Intent intent = getIntent();
426        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
427            int extraVideoQuality = intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
428            videoQualityHigh = (extraVideoQuality > 0);
429        }
430
431        if (videoQualityHigh) {
432            // CIF size
433            mVideoWidth = 352;
434            mVideoHeight = 288;
435        } else {
436            // QCIF size
437            mVideoWidth = 176;
438            mVideoHeight = 144;
439        }
440    }
441
442    @Override
443    public void onResume() {
444        super.onResume();
445        mPausing = false;
446
447        setScreenTimeoutLong();
448        readVideoSizePreference();
449
450        // install an intent filter to receive SD card related events.
451        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
452        intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
453        intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
454        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
455        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
456        intentFilter.addDataScheme("file");
457        registerReceiver(mReceiver, intentFilter);
458        mStorageStatus = getStorageStatus(true);
459
460        mHandler.postDelayed(new Runnable() {
461            public void run() {
462                showStorageHint();
463            }
464        }, 200);
465
466        if (mSurfaceHolder != null) {
467            startPreview();
468            mRecorderInitialized = false;
469            mHandler.sendEmptyMessage(INIT_RECORDER);
470        }
471    }
472
473    private void setCameraParameters() {
474        android.hardware.Camera.Parameters param;
475        param = mCameraDevice.getParameters();
476        param.setPreviewSize(mVideoWidth, mVideoHeight);
477        mCameraDevice.setParameters(param);
478    }
479
480    // Precondition: mSurfaceHolder != null
481    private void startPreview() {
482        Log.v(TAG, "startPreview");
483        if (mPreviewing) {
484            // After recording a video, preview is not stopped. So just return.
485            return;
486        }
487
488        if (mCameraDevice == null) {
489            // If the activity is paused and resumed, camera device has been
490            // released and we need to open the camera.
491            mCameraDevice = CameraHolder.instance().open();
492        }
493
494        setCameraParameters();
495        try {
496            mCameraDevice.setPreviewDisplay(mSurfaceHolder);
497        } catch (Throwable ex) {
498            closeCamera();
499            throw new RuntimeException("setPreviewDisplay failed", ex);
500        }
501
502        try {
503            mCameraDevice.startPreview();
504            mPreviewing = true;
505        } catch (Throwable ex) {
506            closeCamera();
507            throw new RuntimeException("startPreview failed", ex);
508        }
509        mCameraDevice.unlock();
510    }
511
512    private void closeCamera() {
513        Log.v(TAG, "closeCamera");
514        if (mCameraDevice == null) {
515            Log.d(TAG, "already stopped.");
516            return;
517        }
518        // If we don't lock the camera, release() will fail.
519        mCameraDevice.lock();
520        CameraHolder.instance().release();
521        mCameraDevice = null;
522        mPreviewing = false;
523    }
524
525    @Override
526    public void onStop() {
527        setScreenTimeoutSystemDefault();
528        super.onStop();
529    }
530
531    @Override
532    protected void onPause() {
533        super.onPause();
534
535        mPausing = true;
536
537        // This is similar to what mShutterButton.performClick() does,
538        // but not quite the same.
539        if (mMediaRecorderRecording) {
540            if (mIsVideoCaptureIntent) {
541                stopVideoRecording();
542                showAlert();
543            } else {
544                stopVideoRecordingAndGetThumbnail();
545            }
546        } else {
547            stopVideoRecording();
548        }
549        closeCamera();
550
551        unregisterReceiver(mReceiver);
552        setScreenTimeoutSystemDefault();
553
554        if (!mIsVideoCaptureIntent) {
555            mThumbController.storeData(ImageManager.getLastVideoThumbPath());
556        }
557
558        if (mStorageHint != null) {
559            mStorageHint.cancel();
560            mStorageHint = null;
561        }
562
563        mHandler.removeMessages(INIT_RECORDER);
564    }
565
566    @Override
567    public boolean onKeyDown(int keyCode, KeyEvent event) {
568        // Do not handle any key if the activity is paused.
569        if (mPausing) {
570            return true;
571        }
572
573        setScreenTimeoutLong();
574
575        switch (keyCode) {
576            case KeyEvent.KEYCODE_BACK:
577                if (mMediaRecorderRecording) {
578                    mShutterButton.performClick();
579                    return true;
580                }
581                break;
582            case KeyEvent.KEYCODE_CAMERA:
583                if (event.getRepeatCount() == 0) {
584                    mShutterButton.performClick();
585                    return true;
586                }
587                break;
588            case KeyEvent.KEYCODE_DPAD_CENTER:
589                if (event.getRepeatCount() == 0) {
590                    mShutterButton.performClick();
591                    return true;
592                }
593                break;
594            case KeyEvent.KEYCODE_MENU:
595                if (mMediaRecorderRecording) {
596                    mShutterButton.performClick();
597                    return true;
598                }
599                break;
600        }
601
602        return super.onKeyDown(keyCode, event);
603    }
604
605    @Override
606    public boolean onKeyUp(int keyCode, KeyEvent event) {
607        switch (keyCode) {
608            case KeyEvent.KEYCODE_CAMERA:
609                mShutterButton.setPressed(false);
610                return true;
611        }
612        return super.onKeyUp(keyCode, event);
613    }
614
615    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
616        if (mPausing) {
617            // We're pausing, the screen is off and we already stopped
618            // video recording. We don't want to start the camera again
619            // in this case in order to conserve power.
620            // The fact that surfaceChanged is called _after_ an onPause appears
621            // to be legitimate since in that case the lockscreen always returns
622            // to portrait orientation possibly triggering the notification.
623            return;
624        }
625
626        if (mMediaRecorderRecording) {
627            stopVideoRecording();
628        }
629
630        // Start the preview if it is not started yet. Preview may be already
631        // started in onResume and then surfaceChanged is called due to
632        // orientation change.
633        if (!mPreviewing) {
634            startPreview();
635            mRecorderInitialized = false;
636            mHandler.sendEmptyMessage(INIT_RECORDER);
637        }
638    }
639
640    public void surfaceCreated(SurfaceHolder holder) {
641        mSurfaceHolder = holder;
642    }
643
644    public void surfaceDestroyed(SurfaceHolder holder) {
645        mSurfaceHolder = null;
646    }
647
648    private void gotoGallery() {
649        MenuHelper.gotoCameraVideoGallery(this);
650    }
651
652    @Override
653    public boolean onPrepareOptionsMenu(Menu menu) {
654        super.onPrepareOptionsMenu(menu);
655
656        for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) {
657            if (i != MenuHelper.GENERIC_ITEM) {
658                menu.setGroupVisible(i, false);
659            }
660        }
661
662        menu.setGroupVisible(MenuHelper.VIDEO_MODE_ITEM, true);
663        return true;
664    }
665
666    @Override
667    public boolean onCreateOptionsMenu(Menu menu) {
668        super.onCreateOptionsMenu(menu);
669
670        if (mIsVideoCaptureIntent) {
671            // No options menu for attach mode.
672            return false;
673        } else {
674            addBaseMenuItems(menu);
675            int menuFlags =
676                    MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU
677                            & ~MenuHelper.INCLUDE_DETAILS_MENU;
678            MenuHelper.addImageMenuItems(menu, menuFlags, false, VideoCamera.this, mHandler,
679            // Handler for deletion
680                    new Runnable() {
681                        public void run() {
682                            // What do we do here?
683                            // mContentResolver.delete(uri, null, null);
684                        }
685                    }, new MenuHelper.MenuInvoker() {
686                        public void run(final MenuHelper.MenuCallback cb) {
687                        }
688                    });
689
690            MenuItem gallery =
691                    menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0,
692                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
693                            new MenuItem.OnMenuItemClickListener() {
694                                public boolean onMenuItemClick(MenuItem item) {
695                                    gotoGallery();
696                                    return true;
697                                }
698                            });
699            gallery.setIcon(android.R.drawable.ic_menu_gallery);
700        }
701        return true;
702    }
703
704    private boolean isVideoCaptureIntent() {
705        String action = getIntent().getAction();
706        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
707    }
708
709    private void doReturnToCaller(boolean success) {
710        Intent resultIntent = new Intent();
711        int resultCode;
712        if (success) {
713            resultCode = RESULT_OK;
714            resultIntent.setData(mCurrentVideoUri);
715        } else {
716            resultCode = RESULT_CANCELED;
717        }
718        setResult(resultCode, resultIntent);
719        finish();
720    }
721
722    /**
723     * Returns
724     *
725     * @return number of bytes available, or an ERROR code.
726     */
727    private static long getAvailableStorage() {
728        try {
729            if (!ImageManager.hasStorage()) {
730                return NO_STORAGE_ERROR;
731            } else {
732                String storageDirectory = Environment.getExternalStorageDirectory().toString();
733                StatFs stat = new StatFs(storageDirectory);
734                return ((long) stat.getAvailableBlocks() * (long) stat.getBlockSize());
735            }
736        } catch (RuntimeException ex) {
737            // if we can't stat the filesystem then we don't know how many
738            // free bytes exist. It might be zero but just leave it
739            // blank since we really don't know.
740            return CANNOT_STAT_ERROR;
741        }
742    }
743
744    private void cleanupEmptyFile() {
745        if (mCameraVideoFilename != null) {
746            File f = new File(mCameraVideoFilename);
747            if (f.length() == 0 && f.delete()) {
748                Log.v(TAG, "Empty video file deleted: " + mCameraVideoFilename);
749                mCameraVideoFilename = null;
750            }
751        }
752    }
753
754    private android.hardware.Camera mCameraDevice;
755
756    // initializeRecorder() prepares media recorder. Return false if fails.
757    private boolean initializeRecorder() {
758        Log.v(TAG, "initializeRecorder");
759        if (mRecorderInitialized) return true;
760
761        // We will call initializeRecorder() again when the alert is hidden.
762        if (isAlertVisible()) {
763            return false;
764        }
765
766        Intent intent = getIntent();
767        Bundle myExtras = intent.getExtras();
768
769        if (mIsVideoCaptureIntent && myExtras != null) {
770            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
771            if (saveUri != null) {
772                try {
773                    mCameraVideoFileDescriptor =
774                            mContentResolver.openFileDescriptor(saveUri, "rw").getFileDescriptor();
775                    mCurrentVideoUri = saveUri;
776                } catch (java.io.FileNotFoundException ex) {
777                    // invalid uri
778                    Log.e(TAG, ex.toString());
779                }
780            }
781        }
782        releaseMediaRecorder();
783
784        mMediaRecorder = new MediaRecorder();
785
786        mMediaRecorder.setCamera(mCameraDevice);
787        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
788        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
789        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
790        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
791
792        if (mStorageStatus != STORAGE_STATUS_OK) {
793            mMediaRecorder.setOutputFile("/dev/null");
794        } else {
795            // We try Uri in intent first. If it doesn't work, use our own
796            // instead.
797            if (mCameraVideoFileDescriptor != null) {
798                mMediaRecorder.setOutputFile(mCameraVideoFileDescriptor);
799            } else {
800                createVideoPath();
801                mMediaRecorder.setOutputFile(mCameraVideoFilename);
802            }
803        }
804
805        // Use the same frame rate for both, since internally
806        // if the frame rate is too large, it can cause camera to become
807        // unstable. We need to fix the MediaRecorder to disable the support
808        // of setting frame rate for now.
809        mMediaRecorder.setVideoFrameRate(20);
810        mMediaRecorder.setVideoSize(mVideoWidth, mVideoHeight);
811        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263);
812        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
813        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
814
815        long remaining = getAvailableStorage();
816        // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter
817        // of that to make it more likely that recording can complete
818        // successfully.
819        try {
820            mMediaRecorder.setMaxFileSize(remaining - LOW_STORAGE_THRESHOLD / 4);
821        } catch (RuntimeException exception) {
822            // We are going to ignore failure of setMaxFileSize here, as
823            // a) The composer selected may simply not support it, or
824            // b) The underlying media framework may not handle 64-bit range
825            // on the size restriction.
826        }
827
828        try {
829            mMediaRecorder.prepare();
830        } catch (IOException exception) {
831            Log.e(TAG, "prepare failed for " + mCameraVideoFilename);
832            releaseMediaRecorder();
833            // TODO: add more exception handling logic here
834            return false;
835        }
836        mMediaRecorderRecording = false;
837
838        // Update the last video thumbnail.
839        if (!mIsVideoCaptureIntent) {
840            if (!mThumbController.isUriValid()) {
841                updateLastVideo();
842            }
843            mThumbController.updateDisplayIfNeeded();
844        }
845        mRecorderInitialized = true;
846        return true;
847    }
848
849    private void releaseMediaRecorder() {
850        Log.v(TAG, "Releasing media recorder.");
851        if (mMediaRecorder != null) {
852            cleanupEmptyFile();
853            mMediaRecorder.reset();
854            mMediaRecorder.release();
855            mMediaRecorder = null;
856        }
857    }
858
859    private int getIntPreference(String key, int defaultValue) {
860        String s = mPreferences.getString(key, "");
861        int result = defaultValue;
862        try {
863            result = Integer.parseInt(s);
864        } catch (NumberFormatException e) {
865            // Ignore, result is already the default value.
866        }
867        return result;
868    }
869
870    private boolean getBooleanPreference(String key, boolean defaultValue) {
871        return getIntPreference(key, defaultValue ? 1 : 0) != 0;
872    }
873
874    private void createVideoPath() {
875        long dateTaken = System.currentTimeMillis();
876        String title = createName(dateTaken);
877        String displayName = title + ".3gp"; // Used when emailing.
878        String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME;
879        File cameraDir = new File(cameraDirPath);
880        cameraDir.mkdirs();
881        SimpleDateFormat dateFormat =
882                new SimpleDateFormat(getString(R.string.video_file_name_format));
883        Date date = new Date(dateTaken);
884        String filepart = dateFormat.format(date);
885        String filename = cameraDirPath + "/" + filepart + ".3gp";
886        ContentValues values = new ContentValues(7);
887        values.put(Video.Media.TITLE, title);
888        values.put(Video.Media.DISPLAY_NAME, displayName);
889        values.put(Video.Media.DATE_TAKEN, dateTaken);
890        values.put(Video.Media.MIME_TYPE, "video/3gpp");
891        values.put(Video.Media.DATA, filename);
892        mCameraVideoFilename = filename;
893        Log.v(TAG, "Current camera video filename: " + mCameraVideoFilename);
894        mCurrentVideoValues = values;
895    }
896
897    private void registerVideo() {
898        if (mCameraVideoFileDescriptor == null) {
899            Uri videoTable = Uri.parse("content://media/external/video/media");
900            mCurrentVideoValues.put(Video.Media.SIZE, new File(mCurrentVideoFilename).length());
901            mCurrentVideoUri = mContentResolver.insert(videoTable, mCurrentVideoValues);
902            Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
903        }
904        mCurrentVideoValues = null;
905    }
906
907    private void deleteCurrentVideo() {
908        if (mCurrentVideoFilename != null) {
909            deleteVideoFile(mCurrentVideoFilename);
910            mCurrentVideoFilename = null;
911        }
912        if (mCurrentVideoUri != null) {
913            mContentResolver.delete(mCurrentVideoUri, null, null);
914            mCurrentVideoUri = null;
915        }
916        updateAndShowStorageHint(true);
917    }
918
919    private void deleteVideoFile(String fileName) {
920        Log.v(TAG, "Deleting video " + fileName);
921        File f = new File(fileName);
922        if (!f.delete()) {
923            Log.v(TAG, "Could not delete " + fileName);
924        }
925    }
926
927    private void addBaseMenuItems(Menu menu) {
928        MenuHelper.addSwitchModeMenuItem(menu, this, false);
929        {
930            MenuItem gallery =
931                    menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0,
932                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
933                            new OnMenuItemClickListener() {
934                                public boolean onMenuItemClick(MenuItem item) {
935                                    gotoGallery();
936                                    return true;
937                                }
938                            });
939            gallery.setIcon(android.R.drawable.ic_menu_gallery);
940            mGalleryItems.add(gallery);
941        }
942        {
943            MenuItem gallery =
944                    menu.add(MenuHelper.VIDEO_MODE_ITEM, MENU_GALLERY_VIDEOS, 0,
945                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
946                            new OnMenuItemClickListener() {
947                                public boolean onMenuItemClick(MenuItem item) {
948                                    gotoGallery();
949                                    return true;
950                                }
951                            });
952            gallery.setIcon(android.R.drawable.ic_menu_gallery);
953            mGalleryItems.add(gallery);
954        }
955
956        MenuItem item =
957                menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings)
958                        .setOnMenuItemClickListener(new OnMenuItemClickListener() {
959                            public boolean onMenuItemClick(MenuItem item) {
960                                // Keep the camera instance for a while.
961                                // This avoids re-opening the camera and saves
962                                // time.
963                                CameraHolder.instance().keep();
964
965                                Intent intent = new Intent();
966                                intent.setClass(VideoCamera.this, CameraSettings.class);
967                                startActivity(intent);
968                                return true;
969                            }
970                        });
971        item.setIcon(android.R.drawable.ic_menu_preferences);
972    }
973
974    // from MediaRecorder.OnErrorListener
975    public void onError(MediaRecorder mr, int what, int extra) {
976        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
977            // We may have run out of space on the sdcard.
978            stopVideoRecording();
979            updateAndShowStorageHint(true);
980        }
981    }
982
983    // from MediaRecorder.OnInfoListener
984    public void onInfo(MediaRecorder mr, int what, int extra) {
985        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
986            mShutterButton.performClick();
987        } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
988            mShutterButton.performClick();
989            updateAndShowStorageHint(true);
990        }
991    }
992
993    /*
994     * Make sure we're not recording music playing in the background, ask the
995     * MediaPlaybackService to pause playback.
996     */
997    private void pauseAudioPlayback() {
998        // Shamelessly copied from MediaPlaybackService.java, which
999        // should be public, but isn't.
1000        Intent i = new Intent("com.android.music.musicservicecommand");
1001        i.putExtra("command", "pause");
1002
1003        sendBroadcast(i);
1004    }
1005
1006    private void startVideoRecording() {
1007        Log.v(TAG, "startVideoRecording");
1008        if (!mMediaRecorderRecording) {
1009
1010            if (mStorageStatus != STORAGE_STATUS_OK) {
1011                Log.v(TAG, "Storage issue, ignore the start request");
1012                return;
1013            }
1014
1015            // Check mMediaRecorder to see whether it is initialized or not.
1016            if (mMediaRecorder == null) {
1017                Log.e(TAG, "MediaRecorder is not initialized.");
1018                return;
1019            }
1020
1021            pauseAudioPlayback();
1022
1023            try {
1024                mMediaRecorder.setOnErrorListener(this);
1025                mMediaRecorder.setOnInfoListener(this);
1026                mMediaRecorder.start(); // Recording is now started
1027            } catch (RuntimeException e) {
1028                Log.e(TAG, "Could not start media recorder. ", e);
1029                return;
1030            }
1031            mMediaRecorderRecording = true;
1032            mRecordingStartTime = SystemClock.uptimeMillis();
1033            updateRecordingIndicator(false);
1034            mRecordingTimeView.setText("");
1035            mRecordingTimeView.setVisibility(View.VISIBLE);
1036            mHandler.sendEmptyMessage(UPDATE_RECORD_TIME);
1037            setScreenTimeoutInfinite();
1038        }
1039    }
1040
1041    private void updateRecordingIndicator(boolean showRecording) {
1042        int drawableId =
1043                showRecording ? R.drawable.btn_ic_video_record
1044                        : R.drawable.btn_ic_video_record_stop;
1045        Drawable drawable = getResources().getDrawable(drawableId);
1046        mShutterButton.setImageDrawable(drawable);
1047    }
1048
1049    private void stopVideoRecordingAndGetThumbnail() {
1050        stopVideoRecording();
1051        acquireVideoThumb();
1052    }
1053
1054    private void stopVideoRecordingAndShowAlert() {
1055        stopVideoRecording();
1056        showAlert();
1057    }
1058
1059    private void showAlert() {
1060        mVideoPreview.setVisibility(View.INVISIBLE);
1061        fadeOut(findViewById(R.id.shutter_button));
1062        if (mCurrentVideoFilename != null) {
1063            mVideoFrame.setImageBitmap(
1064                    Util.createVideoThumbnail(mCurrentVideoFilename));
1065            mVideoFrame.setVisibility(View.VISIBLE);
1066        }
1067        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1068        for (int id : pickIds) {
1069            View button = findViewById(id);
1070            fadeIn(((View) button.getParent()));
1071        }
1072    }
1073
1074    private void hideAlert() {
1075        mVideoPreview.setVisibility(View.VISIBLE);
1076        mVideoFrame.setVisibility(View.INVISIBLE);
1077        fadeIn(findViewById(R.id.shutter_button));
1078        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1079        for (int id : pickIds) {
1080            View button = findViewById(id);
1081            fadeOut(((View) button.getParent()));
1082        }
1083    }
1084
1085    private static void fadeIn(View view) {
1086        view.setVisibility(View.VISIBLE);
1087        Animation animation = new AlphaAnimation(0F, 1F);
1088        animation.setDuration(500);
1089        view.startAnimation(animation);
1090    }
1091
1092    private static void fadeOut(View view) {
1093        view.setVisibility(View.INVISIBLE);
1094        Animation animation = new AlphaAnimation(1F, 0F);
1095        animation.setDuration(500);
1096        view.startAnimation(animation);
1097    }
1098
1099    private boolean isAlertVisible() {
1100        return this.mVideoFrame.getVisibility() == View.VISIBLE;
1101    }
1102
1103    private void stopVideoRecordingAndShowReview() {
1104        stopVideoRecording();
1105        if (mThumbController.isUriValid()) {
1106            Uri targetUri = mThumbController.getUri();
1107            Intent intent = new Intent(this, ReviewImage.class);
1108            intent.setData(targetUri);
1109            intent.putExtra(MediaStore.EXTRA_FULL_SCREEN, true);
1110            intent.putExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, true);
1111            intent.putExtra("com.android.camera.ReviewMode", true);
1112            try {
1113                startActivity(intent);
1114            } catch (ActivityNotFoundException ex) {
1115                Log.e(TAG, "review video fail", ex);
1116            }
1117        } else {
1118            Log.e(TAG, "Can't view last video.");
1119        }
1120    }
1121
1122    private void stopVideoRecording() {
1123        Log.v(TAG, "stopVideoRecording");
1124        boolean needToRegisterRecording = false;
1125        if (mMediaRecorderRecording || mMediaRecorder != null) {
1126            if (mMediaRecorderRecording && mMediaRecorder != null) {
1127                try {
1128                    mMediaRecorder.setOnErrorListener(null);
1129                    mMediaRecorder.setOnInfoListener(null);
1130                    mMediaRecorder.stop();
1131                } catch (RuntimeException e) {
1132                    Log.e(TAG, "stop fail: " + e.getMessage());
1133                }
1134
1135                mCurrentVideoFilename = mCameraVideoFilename;
1136                Log.v(TAG, "Setting current video filename: " + mCurrentVideoFilename);
1137                needToRegisterRecording = true;
1138                mMediaRecorderRecording = false;
1139            }
1140            releaseMediaRecorder();
1141            updateRecordingIndicator(true);
1142            mRecordingTimeView.setVisibility(View.GONE);
1143            setScreenTimeoutLong();
1144        }
1145        if (needToRegisterRecording && mStorageStatus == STORAGE_STATUS_OK) {
1146            registerVideo();
1147        }
1148
1149        mCameraVideoFilename = null;
1150        mCameraVideoFileDescriptor = null;
1151    }
1152
1153    private void setScreenTimeoutSystemDefault() {
1154        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1155        clearScreenOnFlag();
1156    }
1157
1158    private void setScreenTimeoutLong() {
1159        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1160        setScreenOnFlag();
1161        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1162    }
1163
1164    private void setScreenTimeoutInfinite() {
1165        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1166        setScreenOnFlag();
1167    }
1168
1169    private void clearScreenOnFlag() {
1170        Window w = getWindow();
1171        final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
1172        if ((w.getAttributes().flags & flag) != 0) {
1173            w.clearFlags(flag);
1174        }
1175    }
1176
1177    private void setScreenOnFlag() {
1178        Window w = getWindow();
1179        final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
1180        if ((w.getAttributes().flags & flag) == 0) {
1181            w.addFlags(flag);
1182        }
1183    }
1184
1185    private void hideAlertAndInitializeRecorder() {
1186        hideAlert();
1187        mRecorderInitialized = false;
1188        mHandler.sendEmptyMessage(INIT_RECORDER);
1189    }
1190
1191    private void acquireVideoThumb() {
1192        Bitmap videoFrame = Util.createVideoThumbnail(mCurrentVideoFilename);
1193        mThumbController.setData(mCurrentVideoUri, videoFrame);
1194    }
1195
1196    private static ImageManager.DataLocation dataLocation() {
1197        return ImageManager.DataLocation.EXTERNAL;
1198    }
1199
1200    private void updateLastVideo() {
1201        IImageList list =
1202                ImageManager.allImages(mContentResolver, dataLocation(),
1203                        ImageManager.INCLUDE_VIDEOS, ImageManager.SORT_ASCENDING,
1204                        ImageManager.CAMERA_IMAGE_BUCKET_ID);
1205        int count = list.getCount();
1206        if (count > 0) {
1207            IImage image = list.getImageAt(count - 1);
1208            Uri uri = image.fullSizeImageUri();
1209            mThumbController.setData(uri, image.miniThumbBitmap());
1210        } else {
1211            mThumbController.setData(null, null);
1212        }
1213        list.deactivate();
1214    }
1215
1216    private void updateRecordingTime() {
1217        if (!mMediaRecorderRecording) {
1218            return;
1219        }
1220        long now = SystemClock.uptimeMillis();
1221        long delta = now - mRecordingStartTime;
1222
1223        // Starting a minute before reaching the max duration
1224        // limit, we'll countdown the remaining time instead.
1225        boolean countdownRemainingTime = (delta >= mMaxVideoDurationInMs - 60000);
1226
1227        if (countdownRemainingTime) {
1228            delta = Math.max(0, mMaxVideoDurationInMs - delta);
1229        }
1230
1231        long seconds = (delta + 500) / 1000; // round to nearest
1232        long minutes = seconds / 60;
1233        long hours = minutes / 60;
1234        long remainderMinutes = minutes - (hours * 60);
1235        long remainderSeconds = seconds - (minutes * 60);
1236
1237        String secondsString = Long.toString(remainderSeconds);
1238        if (secondsString.length() < 2) {
1239            secondsString = "0" + secondsString;
1240        }
1241        String minutesString = Long.toString(remainderMinutes);
1242        if (minutesString.length() < 2) {
1243            minutesString = "0" + minutesString;
1244        }
1245        String text = minutesString + ":" + secondsString;
1246        if (hours > 0) {
1247            String hoursString = Long.toString(hours);
1248            if (hoursString.length() < 2) {
1249                hoursString = "0" + hoursString;
1250            }
1251            text = hoursString + ":" + text;
1252        }
1253        mRecordingTimeView.setText(text);
1254
1255        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1256            // Avoid setting the color on every update, do it only
1257            // when it needs changing.
1258
1259            mRecordingTimeCountsDown = countdownRemainingTime;
1260
1261            int color =
1262                    getResources().getColor(
1263                            countdownRemainingTime ? R.color.recording_time_remaining_text
1264                                    : R.color.recording_time_elapsed_text);
1265
1266            mRecordingTimeView.setTextColor(color);
1267        }
1268
1269        // Work around a limitation of the T-Mobile G1: The T-Mobile
1270        // hardware blitter can't pixel-accurately scale and clip at the
1271        // same time, and the SurfaceFlinger doesn't attempt to work around
1272        // this limitation. In order to avoid visual corruption we must
1273        // manually refresh the entire surface view when changing any
1274        // overlapping view's contents.
1275        mVideoPreview.invalidate();
1276        mHandler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000);
1277    }
1278}
1279