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