VideoCamera.java revision 37dcfbb99d78c5179ecda79e7fd408eca4d00844
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        // Make sure we have a surface in the holder before proceeding.
617        if (holder.getSurface() == null) {
618            Log.d(TAG, "holder.getSurface() == null");
619            return;
620        }
621
622        if (mPausing) {
623            // We're pausing, the screen is off and we already stopped
624            // video recording. We don't want to start the camera again
625            // in this case in order to conserve power.
626            // The fact that surfaceChanged is called _after_ an onPause appears
627            // to be legitimate since in that case the lockscreen always returns
628            // to portrait orientation possibly triggering the notification.
629            return;
630        }
631
632        if (mMediaRecorderRecording) {
633            stopVideoRecording();
634        }
635
636        // Start the preview if it is not started yet. Preview may be already
637        // started in onResume and then surfaceChanged is called due to
638        // orientation change.
639        if (!mPreviewing) {
640            startPreview();
641            mRecorderInitialized = false;
642            mHandler.sendEmptyMessage(INIT_RECORDER);
643        }
644    }
645
646    public void surfaceCreated(SurfaceHolder holder) {
647        mSurfaceHolder = holder;
648    }
649
650    public void surfaceDestroyed(SurfaceHolder holder) {
651        mSurfaceHolder = null;
652    }
653
654    private void gotoGallery() {
655        MenuHelper.gotoCameraVideoGallery(this);
656    }
657
658    @Override
659    public boolean onPrepareOptionsMenu(Menu menu) {
660        super.onPrepareOptionsMenu(menu);
661
662        for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) {
663            if (i != MenuHelper.GENERIC_ITEM) {
664                menu.setGroupVisible(i, false);
665            }
666        }
667
668        menu.setGroupVisible(MenuHelper.VIDEO_MODE_ITEM, true);
669        return true;
670    }
671
672    @Override
673    public boolean onCreateOptionsMenu(Menu menu) {
674        super.onCreateOptionsMenu(menu);
675
676        if (mIsVideoCaptureIntent) {
677            // No options menu for attach mode.
678            return false;
679        } else {
680            addBaseMenuItems(menu);
681            int menuFlags =
682                    MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU
683                            & ~MenuHelper.INCLUDE_DETAILS_MENU;
684            MenuHelper.addImageMenuItems(menu, menuFlags, false, VideoCamera.this, mHandler,
685            // Handler for deletion
686                    new Runnable() {
687                        public void run() {
688                            // What do we do here?
689                            // mContentResolver.delete(uri, null, null);
690                        }
691                    }, new MenuHelper.MenuInvoker() {
692                        public void run(final MenuHelper.MenuCallback cb) {
693                        }
694                    });
695
696            MenuItem gallery =
697                    menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0,
698                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
699                            new MenuItem.OnMenuItemClickListener() {
700                                public boolean onMenuItemClick(MenuItem item) {
701                                    gotoGallery();
702                                    return true;
703                                }
704                            });
705            gallery.setIcon(android.R.drawable.ic_menu_gallery);
706        }
707        return true;
708    }
709
710    private boolean isVideoCaptureIntent() {
711        String action = getIntent().getAction();
712        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
713    }
714
715    private void doReturnToCaller(boolean success) {
716        Intent resultIntent = new Intent();
717        int resultCode;
718        if (success) {
719            resultCode = RESULT_OK;
720            resultIntent.setData(mCurrentVideoUri);
721        } else {
722            resultCode = RESULT_CANCELED;
723        }
724        setResult(resultCode, resultIntent);
725        finish();
726    }
727
728    /**
729     * Returns
730     *
731     * @return number of bytes available, or an ERROR code.
732     */
733    private static long getAvailableStorage() {
734        try {
735            if (!ImageManager.hasStorage()) {
736                return NO_STORAGE_ERROR;
737            } else {
738                String storageDirectory = Environment.getExternalStorageDirectory().toString();
739                StatFs stat = new StatFs(storageDirectory);
740                return ((long) stat.getAvailableBlocks() * (long) stat.getBlockSize());
741            }
742        } catch (RuntimeException ex) {
743            // if we can't stat the filesystem then we don't know how many
744            // free bytes exist. It might be zero but just leave it
745            // blank since we really don't know.
746            return CANNOT_STAT_ERROR;
747        }
748    }
749
750    private void cleanupEmptyFile() {
751        if (mCameraVideoFilename != null) {
752            File f = new File(mCameraVideoFilename);
753            if (f.length() == 0 && f.delete()) {
754                Log.v(TAG, "Empty video file deleted: " + mCameraVideoFilename);
755                mCameraVideoFilename = null;
756            }
757        }
758    }
759
760    private android.hardware.Camera mCameraDevice;
761
762    // initializeRecorder() prepares media recorder. Return false if fails.
763    private boolean initializeRecorder() {
764        Log.v(TAG, "initializeRecorder");
765        if (mRecorderInitialized) return true;
766
767        // We will call initializeRecorder() again when the alert is hidden.
768        if (isAlertVisible()) {
769            return false;
770        }
771
772        Intent intent = getIntent();
773        Bundle myExtras = intent.getExtras();
774
775        if (mIsVideoCaptureIntent && myExtras != null) {
776            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
777            if (saveUri != null) {
778                try {
779                    mCameraVideoFileDescriptor =
780                            mContentResolver.openFileDescriptor(saveUri, "rw").getFileDescriptor();
781                    mCurrentVideoUri = saveUri;
782                } catch (java.io.FileNotFoundException ex) {
783                    // invalid uri
784                    Log.e(TAG, ex.toString());
785                }
786            }
787        }
788        releaseMediaRecorder();
789
790        mMediaRecorder = new MediaRecorder();
791
792        mMediaRecorder.setCamera(mCameraDevice);
793        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
794        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
795        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
796        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
797
798        if (mStorageStatus != STORAGE_STATUS_OK) {
799            mMediaRecorder.setOutputFile("/dev/null");
800        } else {
801            // We try Uri in intent first. If it doesn't work, use our own
802            // instead.
803            if (mCameraVideoFileDescriptor != null) {
804                mMediaRecorder.setOutputFile(mCameraVideoFileDescriptor);
805            } else {
806                createVideoPath();
807                mMediaRecorder.setOutputFile(mCameraVideoFilename);
808            }
809        }
810
811        // Use the same frame rate for both, since internally
812        // if the frame rate is too large, it can cause camera to become
813        // unstable. We need to fix the MediaRecorder to disable the support
814        // of setting frame rate for now.
815        mMediaRecorder.setVideoFrameRate(20);
816        mMediaRecorder.setVideoSize(mVideoWidth, mVideoHeight);
817        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263);
818        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
819        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
820
821        long remaining = getAvailableStorage();
822        // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter
823        // of that to make it more likely that recording can complete
824        // successfully.
825        try {
826            mMediaRecorder.setMaxFileSize(remaining - LOW_STORAGE_THRESHOLD / 4);
827        } catch (RuntimeException exception) {
828            // We are going to ignore failure of setMaxFileSize here, as
829            // a) The composer selected may simply not support it, or
830            // b) The underlying media framework may not handle 64-bit range
831            // on the size restriction.
832        }
833
834        try {
835            mMediaRecorder.prepare();
836        } catch (IOException exception) {
837            Log.e(TAG, "prepare failed for " + mCameraVideoFilename);
838            releaseMediaRecorder();
839            // TODO: add more exception handling logic here
840            return false;
841        }
842        mMediaRecorderRecording = false;
843
844        // Update the last video thumbnail.
845        if (!mIsVideoCaptureIntent) {
846            if (!mThumbController.isUriValid()) {
847                updateLastVideo();
848            }
849            mThumbController.updateDisplayIfNeeded();
850        }
851        mRecorderInitialized = true;
852        return true;
853    }
854
855    private void releaseMediaRecorder() {
856        Log.v(TAG, "Releasing media recorder.");
857        if (mMediaRecorder != null) {
858            cleanupEmptyFile();
859            mMediaRecorder.reset();
860            mMediaRecorder.release();
861            mMediaRecorder = null;
862        }
863    }
864
865    private int getIntPreference(String key, int defaultValue) {
866        String s = mPreferences.getString(key, "");
867        int result = defaultValue;
868        try {
869            result = Integer.parseInt(s);
870        } catch (NumberFormatException e) {
871            // Ignore, result is already the default value.
872        }
873        return result;
874    }
875
876    private boolean getBooleanPreference(String key, boolean defaultValue) {
877        return getIntPreference(key, defaultValue ? 1 : 0) != 0;
878    }
879
880    private void createVideoPath() {
881        long dateTaken = System.currentTimeMillis();
882        String title = createName(dateTaken);
883        String displayName = title + ".3gp"; // Used when emailing.
884        String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME;
885        File cameraDir = new File(cameraDirPath);
886        cameraDir.mkdirs();
887        SimpleDateFormat dateFormat =
888                new SimpleDateFormat(getString(R.string.video_file_name_format));
889        Date date = new Date(dateTaken);
890        String filepart = dateFormat.format(date);
891        String filename = cameraDirPath + "/" + filepart + ".3gp";
892        ContentValues values = new ContentValues(7);
893        values.put(Video.Media.TITLE, title);
894        values.put(Video.Media.DISPLAY_NAME, displayName);
895        values.put(Video.Media.DATE_TAKEN, dateTaken);
896        values.put(Video.Media.MIME_TYPE, "video/3gpp");
897        values.put(Video.Media.DATA, filename);
898        mCameraVideoFilename = filename;
899        Log.v(TAG, "Current camera video filename: " + mCameraVideoFilename);
900        mCurrentVideoValues = values;
901    }
902
903    private void registerVideo() {
904        if (mCameraVideoFileDescriptor == null) {
905            Uri videoTable = Uri.parse("content://media/external/video/media");
906            mCurrentVideoValues.put(Video.Media.SIZE, new File(mCurrentVideoFilename).length());
907            mCurrentVideoUri = mContentResolver.insert(videoTable, mCurrentVideoValues);
908            Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
909        }
910        mCurrentVideoValues = null;
911    }
912
913    private void deleteCurrentVideo() {
914        if (mCurrentVideoFilename != null) {
915            deleteVideoFile(mCurrentVideoFilename);
916            mCurrentVideoFilename = null;
917        }
918        if (mCurrentVideoUri != null) {
919            mContentResolver.delete(mCurrentVideoUri, null, null);
920            mCurrentVideoUri = null;
921        }
922        updateAndShowStorageHint(true);
923    }
924
925    private void deleteVideoFile(String fileName) {
926        Log.v(TAG, "Deleting video " + fileName);
927        File f = new File(fileName);
928        if (!f.delete()) {
929            Log.v(TAG, "Could not delete " + fileName);
930        }
931    }
932
933    private void addBaseMenuItems(Menu menu) {
934        MenuHelper.addSwitchModeMenuItem(menu, this, false);
935        {
936            MenuItem gallery =
937                    menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0,
938                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
939                            new OnMenuItemClickListener() {
940                                public boolean onMenuItemClick(MenuItem item) {
941                                    gotoGallery();
942                                    return true;
943                                }
944                            });
945            gallery.setIcon(android.R.drawable.ic_menu_gallery);
946            mGalleryItems.add(gallery);
947        }
948        {
949            MenuItem gallery =
950                    menu.add(MenuHelper.VIDEO_MODE_ITEM, MENU_GALLERY_VIDEOS, 0,
951                            R.string.camera_gallery_photos_text).setOnMenuItemClickListener(
952                            new OnMenuItemClickListener() {
953                                public boolean onMenuItemClick(MenuItem item) {
954                                    gotoGallery();
955                                    return true;
956                                }
957                            });
958            gallery.setIcon(android.R.drawable.ic_menu_gallery);
959            mGalleryItems.add(gallery);
960        }
961
962        MenuItem item =
963                menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings)
964                        .setOnMenuItemClickListener(new OnMenuItemClickListener() {
965                            public boolean onMenuItemClick(MenuItem item) {
966                                // Keep the camera instance for a while.
967                                // This avoids re-opening the camera and saves
968                                // time.
969                                CameraHolder.instance().keep();
970
971                                Intent intent = new Intent();
972                                intent.setClass(VideoCamera.this, CameraSettings.class);
973                                startActivity(intent);
974                                return true;
975                            }
976                        });
977        item.setIcon(android.R.drawable.ic_menu_preferences);
978    }
979
980    // from MediaRecorder.OnErrorListener
981    public void onError(MediaRecorder mr, int what, int extra) {
982        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
983            // We may have run out of space on the sdcard.
984            stopVideoRecording();
985            updateAndShowStorageHint(true);
986        }
987    }
988
989    // from MediaRecorder.OnInfoListener
990    public void onInfo(MediaRecorder mr, int what, int extra) {
991        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
992            mShutterButton.performClick();
993        } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
994            mShutterButton.performClick();
995            updateAndShowStorageHint(true);
996        }
997    }
998
999    /*
1000     * Make sure we're not recording music playing in the background, ask the
1001     * MediaPlaybackService to pause playback.
1002     */
1003    private void pauseAudioPlayback() {
1004        // Shamelessly copied from MediaPlaybackService.java, which
1005        // should be public, but isn't.
1006        Intent i = new Intent("com.android.music.musicservicecommand");
1007        i.putExtra("command", "pause");
1008
1009        sendBroadcast(i);
1010    }
1011
1012    private void startVideoRecording() {
1013        Log.v(TAG, "startVideoRecording");
1014        if (!mMediaRecorderRecording) {
1015
1016            if (mStorageStatus != STORAGE_STATUS_OK) {
1017                Log.v(TAG, "Storage issue, ignore the start request");
1018                return;
1019            }
1020
1021            // Check mMediaRecorder to see whether it is initialized or not.
1022            if (mMediaRecorder == null) {
1023                Log.e(TAG, "MediaRecorder is not initialized.");
1024                return;
1025            }
1026
1027            pauseAudioPlayback();
1028
1029            try {
1030                mMediaRecorder.setOnErrorListener(this);
1031                mMediaRecorder.setOnInfoListener(this);
1032                mMediaRecorder.start(); // Recording is now started
1033            } catch (RuntimeException e) {
1034                Log.e(TAG, "Could not start media recorder. ", e);
1035                return;
1036            }
1037            mMediaRecorderRecording = true;
1038            mRecordingStartTime = SystemClock.uptimeMillis();
1039            updateRecordingIndicator(false);
1040            mRecordingTimeView.setText("");
1041            mRecordingTimeView.setVisibility(View.VISIBLE);
1042            mHandler.sendEmptyMessage(UPDATE_RECORD_TIME);
1043            setScreenTimeoutInfinite();
1044        }
1045    }
1046
1047    private void updateRecordingIndicator(boolean showRecording) {
1048        int drawableId =
1049                showRecording ? R.drawable.btn_ic_video_record
1050                        : R.drawable.btn_ic_video_record_stop;
1051        Drawable drawable = getResources().getDrawable(drawableId);
1052        mShutterButton.setImageDrawable(drawable);
1053    }
1054
1055    private void stopVideoRecordingAndGetThumbnail() {
1056        stopVideoRecording();
1057        acquireVideoThumb();
1058    }
1059
1060    private void stopVideoRecordingAndShowAlert() {
1061        stopVideoRecording();
1062        showAlert();
1063    }
1064
1065    private void showAlert() {
1066        mVideoPreview.setVisibility(View.INVISIBLE);
1067        fadeOut(findViewById(R.id.shutter_button));
1068        if (mCurrentVideoFilename != null) {
1069            mVideoFrame.setImageBitmap(
1070                    Util.createVideoThumbnail(mCurrentVideoFilename));
1071            mVideoFrame.setVisibility(View.VISIBLE);
1072        }
1073        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1074        for (int id : pickIds) {
1075            View button = findViewById(id);
1076            fadeIn(((View) button.getParent()));
1077        }
1078    }
1079
1080    private void hideAlert() {
1081        mVideoPreview.setVisibility(View.VISIBLE);
1082        mVideoFrame.setVisibility(View.INVISIBLE);
1083        fadeIn(findViewById(R.id.shutter_button));
1084        int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play};
1085        for (int id : pickIds) {
1086            View button = findViewById(id);
1087            fadeOut(((View) button.getParent()));
1088        }
1089    }
1090
1091    private static void fadeIn(View view) {
1092        view.setVisibility(View.VISIBLE);
1093        Animation animation = new AlphaAnimation(0F, 1F);
1094        animation.setDuration(500);
1095        view.startAnimation(animation);
1096    }
1097
1098    private static void fadeOut(View view) {
1099        view.setVisibility(View.INVISIBLE);
1100        Animation animation = new AlphaAnimation(1F, 0F);
1101        animation.setDuration(500);
1102        view.startAnimation(animation);
1103    }
1104
1105    private boolean isAlertVisible() {
1106        return this.mVideoFrame.getVisibility() == View.VISIBLE;
1107    }
1108
1109    private void stopVideoRecordingAndShowReview() {
1110        stopVideoRecording();
1111        if (mThumbController.isUriValid()) {
1112            Uri targetUri = mThumbController.getUri();
1113            Intent intent = new Intent(this, ReviewImage.class);
1114            intent.setData(targetUri);
1115            intent.putExtra(MediaStore.EXTRA_FULL_SCREEN, true);
1116            intent.putExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, true);
1117            intent.putExtra("com.android.camera.ReviewMode", true);
1118            try {
1119                startActivity(intent);
1120            } catch (ActivityNotFoundException ex) {
1121                Log.e(TAG, "review video fail", ex);
1122            }
1123        } else {
1124            Log.e(TAG, "Can't view last video.");
1125        }
1126    }
1127
1128    private void stopVideoRecording() {
1129        Log.v(TAG, "stopVideoRecording");
1130        boolean needToRegisterRecording = false;
1131        if (mMediaRecorderRecording || mMediaRecorder != null) {
1132            if (mMediaRecorderRecording && mMediaRecorder != null) {
1133                try {
1134                    mMediaRecorder.setOnErrorListener(null);
1135                    mMediaRecorder.setOnInfoListener(null);
1136                    mMediaRecorder.stop();
1137                } catch (RuntimeException e) {
1138                    Log.e(TAG, "stop fail: " + e.getMessage());
1139                }
1140
1141                mCurrentVideoFilename = mCameraVideoFilename;
1142                Log.v(TAG, "Setting current video filename: " + mCurrentVideoFilename);
1143                needToRegisterRecording = true;
1144                mMediaRecorderRecording = false;
1145            }
1146            releaseMediaRecorder();
1147            updateRecordingIndicator(true);
1148            mRecordingTimeView.setVisibility(View.GONE);
1149            setScreenTimeoutLong();
1150        }
1151        if (needToRegisterRecording && mStorageStatus == STORAGE_STATUS_OK) {
1152            registerVideo();
1153        }
1154
1155        mCameraVideoFilename = null;
1156        mCameraVideoFileDescriptor = null;
1157    }
1158
1159    private void setScreenTimeoutSystemDefault() {
1160        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1161        clearScreenOnFlag();
1162    }
1163
1164    private void setScreenTimeoutLong() {
1165        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1166        setScreenOnFlag();
1167        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1168    }
1169
1170    private void setScreenTimeoutInfinite() {
1171        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1172        setScreenOnFlag();
1173    }
1174
1175    private void clearScreenOnFlag() {
1176        Window w = getWindow();
1177        final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
1178        if ((w.getAttributes().flags & flag) != 0) {
1179            w.clearFlags(flag);
1180        }
1181    }
1182
1183    private void setScreenOnFlag() {
1184        Window w = getWindow();
1185        final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
1186        if ((w.getAttributes().flags & flag) == 0) {
1187            w.addFlags(flag);
1188        }
1189    }
1190
1191    private void hideAlertAndInitializeRecorder() {
1192        hideAlert();
1193        mRecorderInitialized = false;
1194        mHandler.sendEmptyMessage(INIT_RECORDER);
1195    }
1196
1197    private void acquireVideoThumb() {
1198        Bitmap videoFrame = Util.createVideoThumbnail(mCurrentVideoFilename);
1199        mThumbController.setData(mCurrentVideoUri, videoFrame);
1200    }
1201
1202    private static ImageManager.DataLocation dataLocation() {
1203        return ImageManager.DataLocation.EXTERNAL;
1204    }
1205
1206    private void updateLastVideo() {
1207        IImageList list =
1208                ImageManager.allImages(mContentResolver, dataLocation(),
1209                        ImageManager.INCLUDE_VIDEOS, ImageManager.SORT_ASCENDING,
1210                        ImageManager.CAMERA_IMAGE_BUCKET_ID);
1211        int count = list.getCount();
1212        if (count > 0) {
1213            IImage image = list.getImageAt(count - 1);
1214            Uri uri = image.fullSizeImageUri();
1215            mThumbController.setData(uri, image.miniThumbBitmap());
1216        } else {
1217            mThumbController.setData(null, null);
1218        }
1219        list.deactivate();
1220    }
1221
1222    private void updateRecordingTime() {
1223        if (!mMediaRecorderRecording) {
1224            return;
1225        }
1226        long now = SystemClock.uptimeMillis();
1227        long delta = now - mRecordingStartTime;
1228
1229        // Starting a minute before reaching the max duration
1230        // limit, we'll countdown the remaining time instead.
1231        boolean countdownRemainingTime = (delta >= mMaxVideoDurationInMs - 60000);
1232
1233        if (countdownRemainingTime) {
1234            delta = Math.max(0, mMaxVideoDurationInMs - delta);
1235        }
1236
1237        long seconds = (delta + 500) / 1000; // round to nearest
1238        long minutes = seconds / 60;
1239        long hours = minutes / 60;
1240        long remainderMinutes = minutes - (hours * 60);
1241        long remainderSeconds = seconds - (minutes * 60);
1242
1243        String secondsString = Long.toString(remainderSeconds);
1244        if (secondsString.length() < 2) {
1245            secondsString = "0" + secondsString;
1246        }
1247        String minutesString = Long.toString(remainderMinutes);
1248        if (minutesString.length() < 2) {
1249            minutesString = "0" + minutesString;
1250        }
1251        String text = minutesString + ":" + secondsString;
1252        if (hours > 0) {
1253            String hoursString = Long.toString(hours);
1254            if (hoursString.length() < 2) {
1255                hoursString = "0" + hoursString;
1256            }
1257            text = hoursString + ":" + text;
1258        }
1259        mRecordingTimeView.setText(text);
1260
1261        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1262            // Avoid setting the color on every update, do it only
1263            // when it needs changing.
1264
1265            mRecordingTimeCountsDown = countdownRemainingTime;
1266
1267            int color =
1268                    getResources().getColor(
1269                            countdownRemainingTime ? R.color.recording_time_remaining_text
1270                                    : R.color.recording_time_elapsed_text);
1271
1272            mRecordingTimeView.setTextColor(color);
1273        }
1274
1275        // Work around a limitation of the T-Mobile G1: The T-Mobile
1276        // hardware blitter can't pixel-accurately scale and clip at the
1277        // same time, and the SurfaceFlinger doesn't attempt to work around
1278        // this limitation. In order to avoid visual corruption we must
1279        // manually refresh the entire surface view when changing any
1280        // overlapping view's contents.
1281        mVideoPreview.invalidate();
1282        mHandler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000);
1283    }
1284}
1285