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