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