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