VideoModule.java revision 5b6a934840b370d296046c4929899a43895b1ff8
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import android.annotation.TargetApi;
20import android.app.Activity;
21import android.content.ActivityNotFoundException;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.SharedPreferences.Editor;
29import android.content.res.Configuration;
30import android.graphics.Bitmap;
31import android.hardware.Camera.CameraInfo;
32import android.hardware.Camera.Parameters;
33import android.hardware.Camera.PictureCallback;
34import android.hardware.Camera.Size;
35import android.location.Location;
36import android.media.CamcorderProfile;
37import android.media.CameraProfile;
38import android.media.MediaRecorder;
39import android.net.Uri;
40import android.os.Build;
41import android.os.Bundle;
42import android.os.Handler;
43import android.os.Message;
44import android.os.ParcelFileDescriptor;
45import android.os.SystemClock;
46import android.provider.MediaStore;
47import android.provider.MediaStore.Video;
48import android.util.Log;
49import android.view.Gravity;
50import android.view.KeyEvent;
51import android.view.LayoutInflater;
52import android.view.MotionEvent;
53import android.view.OrientationEventListener;
54import android.view.SurfaceHolder;
55import android.view.View;
56import android.view.View.OnClickListener;
57import android.view.ViewGroup;
58import android.view.WindowManager;
59import android.widget.FrameLayout;
60import android.widget.FrameLayout.LayoutParams;
61import android.widget.ImageView;
62import android.widget.LinearLayout;
63import android.widget.TextView;
64import android.widget.Toast;
65
66import com.android.camera.ui.AbstractSettingPopup;
67import com.android.camera.ui.PieRenderer;
68import com.android.camera.ui.PopupManager;
69import com.android.camera.ui.PreviewSurfaceView;
70import com.android.camera.ui.RenderOverlay;
71import com.android.camera.ui.Rotatable;
72import com.android.camera.ui.RotateImageView;
73import com.android.camera.ui.RotateLayout;
74import com.android.camera.ui.RotateTextToast;
75import com.android.camera.ui.TwoStateImageView;
76import com.android.camera.ui.ZoomRenderer;
77import com.android.gallery3d.common.ApiHelper;
78import com.android.gallery3d.util.AccessibilityUtils;
79
80import java.io.File;
81import java.io.IOException;
82import java.text.SimpleDateFormat;
83import java.util.Date;
84import java.util.Iterator;
85import java.util.List;
86
87public class VideoModule implements CameraModule,
88    CameraPreference.OnPreferenceChangedListener,
89    ShutterButton.OnShutterButtonListener,
90    MediaRecorder.OnErrorListener,
91    MediaRecorder.OnInfoListener,
92    EffectsRecorder.EffectsListener,
93    PieRenderer.PieListener {
94
95    private static final String TAG = "CAM_VideoModule";
96
97    // We number the request code from 1000 to avoid collision with Gallery.
98    private static final int REQUEST_EFFECT_BACKDROPPER = 1000;
99
100    private static final int CHECK_DISPLAY_ROTATION = 3;
101    private static final int CLEAR_SCREEN_DELAY = 4;
102    private static final int UPDATE_RECORD_TIME = 5;
103    private static final int ENABLE_SHUTTER_BUTTON = 6;
104    private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7;
105    private static final int SWITCH_CAMERA = 8;
106    private static final int SWITCH_CAMERA_START_ANIMATION = 9;
107    private static final int HIDE_SURFACE_VIEW = 10;
108
109    private static final int SCREEN_DELAY = 2 * 60 * 1000;
110
111    private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms
112
113    /**
114     * An unpublished intent flag requesting to start recording straight away
115     * and return as soon as recording is stopped.
116     * TODO: consider publishing by moving into MediaStore.
117     */
118    private static final String EXTRA_QUICK_CAPTURE =
119            "android.intent.extra.quickCapture";
120
121    private static final int MIN_THUMB_SIZE = 64;
122    // module fields
123    private CameraActivity mActivity;
124    private View mRootView;
125    private boolean mPaused;
126    private int mCameraId;
127    private Parameters mParameters;
128
129    private boolean mSnapshotInProgress = false;
130
131    private static final String EFFECT_BG_FROM_GALLERY = "gallery";
132
133    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
134
135    private ComboPreferences mPreferences;
136    private PreferenceGroup mPreferenceGroup;
137
138    private PreviewFrameLayout mPreviewFrameLayout;
139    private boolean mSurfaceViewReady;
140    private SurfaceHolder.Callback mSurfaceViewCallback;
141    private PreviewSurfaceView mPreviewSurfaceView;
142    private CameraScreenNail.OnFrameDrawnListener mFrameDrawnListener;
143    private View mReviewControl;
144
145    // An review image having same size as preview. It is displayed when
146    // recording is stopped in capture intent.
147    private ImageView mReviewImage;
148    private Rotatable mReviewCancelButton;
149    private Rotatable mReviewDoneButton;
150    private RotateImageView mReviewPlayButton;
151    private ShutterButton mShutterButton;
152    private TextView mRecordingTimeView;
153    private RotateLayout mBgLearningMessageRotater;
154    private View mBgLearningMessageFrame;
155    private LinearLayout mLabelsLinearLayout;
156
157    private boolean mIsVideoCaptureIntent;
158    private boolean mQuickCapture;
159
160    private MediaRecorder mMediaRecorder;
161    private EffectsRecorder mEffectsRecorder;
162    private boolean mEffectsDisplayResult;
163
164    private int mEffectType = EffectsRecorder.EFFECT_NONE;
165    private Object mEffectParameter = null;
166    private String mEffectUriFromGallery = null;
167    private String mPrefVideoEffectDefault;
168    private boolean mResetEffect = true;
169
170    private boolean mSwitchingCamera;
171    private boolean mMediaRecorderRecording = false;
172    private long mRecordingStartTime;
173    private boolean mRecordingTimeCountsDown = false;
174    private RotateLayout mRecordingTimeRect;
175    private long mOnResumeTime;
176    // The video file that the hardware camera is about to record into
177    // (or is recording into.)
178    private String mVideoFilename;
179    private ParcelFileDescriptor mVideoFileDescriptor;
180
181    // The video file that has already been recorded, and that is being
182    // examined by the user.
183    private String mCurrentVideoFilename;
184    private Uri mCurrentVideoUri;
185    private ContentValues mCurrentVideoValues;
186
187    private CamcorderProfile mProfile;
188
189    // The video duration limit. 0 menas no limit.
190    private int mMaxVideoDurationInMs;
191
192    // Time Lapse parameters.
193    private boolean mCaptureTimeLapse = false;
194    // Default 0. If it is larger than 0, the camcorder is in time lapse mode.
195    private int mTimeBetweenTimeLapseFrameCaptureMs = 0;
196    private View mTimeLapseLabel;
197
198    private int mDesiredPreviewWidth;
199    private int mDesiredPreviewHeight;
200
201    boolean mPreviewing = false; // True if preview is started.
202    // The display rotation in degrees. This is only valid when mPreviewing is
203    // true.
204    private int mDisplayRotation;
205    private int mCameraDisplayOrientation;
206
207    private ContentResolver mContentResolver;
208
209    private LocationManager mLocationManager;
210
211    private VideoNamer mVideoNamer;
212
213    private RenderOverlay mRenderOverlay;
214    private PieRenderer mPieRenderer;
215
216    private VideoController mVideoControl;
217    private AbstractSettingPopup mPopup;
218    private int mPendingSwitchCameraId;
219
220    private ZoomRenderer mZoomRenderer;
221
222    private PreviewGestures mGestures;
223    private View mMenu;
224    private View mBlocker;
225    private View mOnScreenIndicators;
226    private ImageView mFlashIndicator;
227
228    private final Handler mHandler = new MainHandler();
229
230    // The degrees of the device rotated clockwise from its natural orientation.
231    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
232    // The orientation compensation for icons and dialogs. Ex: if the value
233    // is 90, the UI components should be rotated 90 degrees counter-clockwise.
234    private int mOrientationCompensation = 0;
235
236    // If mOrientationResetNeeded is set to be true, onOrientationChanged will reset
237    // the orientation of the on screen indicators to the current orientation compensation
238    // regardless of whether it's the same as the most recent orientation compensation
239    private boolean mOrientationResetNeeded;
240    // The orientation compensation when we start recording.
241    private int mOrientationCompensationAtRecordStart;
242
243    private int mZoomValue;  // The current zoom value.
244    private int mZoomMax;
245    private List<Integer> mZoomRatios;
246    private boolean mRestoreFlash;  // This is used to check if we need to restore the flash
247                                    // status when going back from gallery.
248
249    protected class CameraOpenThread extends Thread {
250        @Override
251        public void run() {
252            openCamera();
253        }
254    }
255
256    private void openCamera() {
257        try {
258            mActivity.mCameraDevice = Util.openCamera(mActivity, mCameraId);
259            mParameters = mActivity.mCameraDevice.getParameters();
260        } catch (CameraHardwareException e) {
261            mActivity.mOpenCameraFail = true;
262        } catch (CameraDisabledException e) {
263            mActivity.mCameraDisabled = true;
264        }
265    }
266
267    // This Handler is used to post message back onto the main thread of the
268    // application
269    private class MainHandler extends Handler {
270        @Override
271        public void handleMessage(Message msg) {
272            switch (msg.what) {
273
274                case ENABLE_SHUTTER_BUTTON:
275                    mShutterButton.setEnabled(true);
276                    break;
277
278                case CLEAR_SCREEN_DELAY: {
279                    mActivity.getWindow().clearFlags(
280                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
281                    break;
282                }
283
284                case UPDATE_RECORD_TIME: {
285                    updateRecordingTime();
286                    break;
287                }
288
289                case CHECK_DISPLAY_ROTATION: {
290                    // Restart the preview if display rotation has changed.
291                    // Sometimes this happens when the device is held upside
292                    // down and camera app is opened. Rotation animation will
293                    // take some time and the rotation value we have got may be
294                    // wrong. Framework does not have a callback for this now.
295                    if ((Util.getDisplayRotation(mActivity) != mDisplayRotation)
296                            && !mMediaRecorderRecording && !mSwitchingCamera) {
297                        startPreview();
298                    }
299                    if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
300                        mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
301                    }
302                    break;
303                }
304
305                case SHOW_TAP_TO_SNAPSHOT_TOAST: {
306                    showTapToSnapshotToast();
307                    break;
308                }
309
310                case SWITCH_CAMERA: {
311                    switchCamera();
312                    break;
313                }
314
315                case SWITCH_CAMERA_START_ANIMATION: {
316                    ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
317
318                    // Enable all camera controls.
319                    mSwitchingCamera = false;
320                    break;
321                }
322
323                case HIDE_SURFACE_VIEW: {
324                    mPreviewSurfaceView.setVisibility(View.GONE);
325                    break;
326                }
327
328                default:
329                    Log.v(TAG, "Unhandled message: " + msg.what);
330                    break;
331            }
332        }
333    }
334
335    private BroadcastReceiver mReceiver = null;
336
337    private class MyBroadcastReceiver extends BroadcastReceiver {
338        @Override
339        public void onReceive(Context context, Intent intent) {
340            String action = intent.getAction();
341            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
342                stopVideoRecording();
343            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
344                Toast.makeText(mActivity,
345                        mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show();
346            }
347        }
348    }
349
350    private String createName(long dateTaken) {
351        Date date = new Date(dateTaken);
352        SimpleDateFormat dateFormat = new SimpleDateFormat(
353                mActivity.getString(R.string.video_file_name_format));
354
355        return dateFormat.format(date);
356    }
357
358    private int getPreferredCameraId(ComboPreferences preferences) {
359        int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
360        if (intentCameraId != -1) {
361            // Testing purpose. Launch a specific camera through the intent
362            // extras.
363            return intentCameraId;
364        } else {
365            return CameraSettings.readPreferredCameraId(preferences);
366        }
367    }
368
369    private void initializeSurfaceView() {
370        mPreviewSurfaceView = (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view);
371        if (!ApiHelper.HAS_SURFACE_TEXTURE) {  // API level < 11
372            if (mSurfaceViewCallback == null) {
373                mSurfaceViewCallback = new SurfaceViewCallback();
374            }
375            mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback);
376            mPreviewSurfaceView.setVisibility(View.VISIBLE);
377        } else if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {  // API level < 16
378            if (mSurfaceViewCallback == null) {
379                mSurfaceViewCallback = new SurfaceViewCallback();
380                mFrameDrawnListener = new CameraScreenNail.OnFrameDrawnListener() {
381                    @Override
382                    public void onFrameDrawn(CameraScreenNail c) {
383                        mHandler.sendEmptyMessage(HIDE_SURFACE_VIEW);
384                    }
385                };
386            }
387            mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback);
388        }
389    }
390
391    private void initializeOverlay() {
392        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
393        if (mPieRenderer == null) {
394            mPieRenderer = new PieRenderer(mActivity);
395            mVideoControl = new VideoController(mActivity, this, mPieRenderer);
396            mVideoControl.setListener(this);
397            mPieRenderer.setPieListener(this);
398        }
399        mRenderOverlay.addRenderer(mPieRenderer);
400        if (mZoomRenderer == null) {
401            mZoomRenderer = new ZoomRenderer(mActivity);
402        }
403        mRenderOverlay.addRenderer(mZoomRenderer);
404        if (mGestures == null) {
405            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
406        }
407        mGestures.setRenderOverlay(mRenderOverlay);
408        mGestures.clearTouchReceivers();
409        mGestures.addTouchReceiver(mMenu);
410        mGestures.addTouchReceiver(mBlocker);
411
412        if (isVideoCaptureIntent()) {
413            if (mReviewCancelButton != null) {
414                mGestures.addTouchReceiver((View) mReviewCancelButton);
415            }
416            if (mReviewDoneButton != null) {
417                mGestures.addTouchReceiver((View) mReviewDoneButton);
418            }
419        }
420    }
421
422    @Override
423    public void init(CameraActivity activity, View root, boolean reuseScreenNail) {
424        mActivity = activity;
425        mRootView = root;
426        mPreferences = new ComboPreferences(mActivity);
427        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
428        mCameraId = getPreferredCameraId(mPreferences);
429
430        mPreferences.setLocalId(mActivity, mCameraId);
431        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
432
433        mActivity.mNumberOfCameras = CameraHolder.instance().getNumberOfCameras();
434        mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default);
435        resetEffect();
436
437        /*
438         * To reduce startup time, we start the preview in another thread.
439         * We make sure the preview is started at the end of onCreate.
440         */
441        CameraOpenThread cameraOpenThread = new CameraOpenThread();
442        cameraOpenThread.start();
443
444        mContentResolver = mActivity.getContentResolver();
445
446        mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView);
447
448        // Surface texture is from camera screen nail and startPreview needs it.
449        // This must be done before startPreview.
450        mIsVideoCaptureIntent = isVideoCaptureIntent();
451        if (reuseScreenNail) {
452            mActivity.reuseCameraScreenNail(!mIsVideoCaptureIntent);
453        } else {
454            mActivity.createCameraScreenNail(!mIsVideoCaptureIntent);
455        }
456        initializeSurfaceView();
457
458        // Make sure camera device is opened.
459        try {
460            cameraOpenThread.join();
461            if (mActivity.mOpenCameraFail) {
462                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
463                return;
464            } else if (mActivity.mCameraDisabled) {
465                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
466                return;
467            }
468        } catch (InterruptedException ex) {
469            // ignore
470        }
471
472        Thread startPreviewThread = new Thread(new Runnable() {
473            @Override
474            public void run() {
475                readVideoPreferences();
476                startPreview();
477            }
478        });
479        startPreviewThread.start();
480
481        initializeControlByIntent();
482        initializeOverlay();
483        initializeMiscControls();
484
485        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
486        mLocationManager = new LocationManager(mActivity, null);
487
488        // Initialize to true to ensure that the on-screen indicators get their
489        // orientation set in onOrientationChanged.
490        mOrientationResetNeeded = true;
491
492        // Make sure preview is started.
493        try {
494            startPreviewThread.join();
495            if (mActivity.mOpenCameraFail) {
496                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
497                return;
498            } else if (mActivity.mCameraDisabled) {
499                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
500                return;
501            }
502        } catch (InterruptedException ex) {
503            // ignore
504        }
505
506        showTimeLapseUI(mCaptureTimeLapse);
507        initializeVideoSnapshot();
508        resizeForPreviewAspectRatio();
509
510        initializeVideoControl();
511        mPendingSwitchCameraId = -1;
512        updateOnScreenIndicators();
513    }
514
515    @Override
516    public void onStop() {}
517
518    private void loadCameraPreferences() {
519        CameraSettings settings = new CameraSettings(mActivity, mParameters,
520                mCameraId, CameraHolder.instance().getCameraInfo());
521        // Remove the video quality preference setting when the quality is given in the intent.
522        mPreferenceGroup = filterPreferenceScreenByIntent(
523                settings.getPreferenceGroup(R.xml.video_preferences));
524    }
525
526    @Override
527    public boolean collapseCameraControls() {
528        boolean ret = false;
529        if (mPopup != null) {
530            dismissPopup(false);
531            ret = true;
532        }
533        return ret;
534    }
535
536    public boolean removeTopLevelPopup() {
537        if (mPopup != null) {
538            dismissPopup(true);
539            return true;
540        }
541        return false;
542    }
543
544    private void enableCameraControls(boolean enable) {
545        if (mGestures != null) {
546            mGestures.setZoomOnly(!enable);
547        }
548        if (mPieRenderer != null && mPieRenderer.showsItems()) {
549            mPieRenderer.hide();
550        }
551    }
552
553    private void initializeVideoControl() {
554        loadCameraPreferences();
555        mVideoControl.initialize(mPreferenceGroup);
556        if (effectsActive()) {
557            mVideoControl.overrideSettings(
558                    CameraSettings.KEY_VIDEO_QUALITY,
559                    Integer.toString(getLowVideoQuality()));
560        }
561    }
562
563    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
564    private static int getLowVideoQuality() {
565        if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) {
566            return CamcorderProfile.QUALITY_480P;
567        } else {
568            return CamcorderProfile.QUALITY_LOW;
569        }
570    }
571
572
573    @Override
574    public void onOrientationChanged(int orientation) {
575        // We keep the last known orientation. So if the user first orient
576        // the camera then point the camera to floor or sky, we still have
577        // the correct orientation.
578        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
579        int newOrientation = Util.roundOrientation(orientation, mOrientation);
580
581        if (mOrientation != newOrientation) {
582            mOrientation = newOrientation;
583            // The input of effects recorder is affected by
584            // android.hardware.Camera.setDisplayOrientation. Its value only
585            // compensates the camera orientation (no Display.getRotation).
586            // So the orientation hint here should only consider sensor
587            // orientation.
588            if (effectsActive()) {
589                mEffectsRecorder.setOrientationHint(mOrientation);
590            }
591        }
592
593        // When the screen is unlocked, display rotation may change. Always
594        // calculate the up-to-date orientationCompensation.
595        int orientationCompensation =
596                (mOrientation + Util.getDisplayRotation(mActivity)) % 360;
597
598        if (mOrientationCompensation != orientationCompensation || mOrientationResetNeeded) {
599            mOrientationCompensation = orientationCompensation;
600            // Do not rotate the icons during recording because the video
601            // orientation is fixed after recording.
602            if (!mMediaRecorderRecording) {
603                setOrientationIndicator(mOrientationCompensation, true);
604                mOrientationResetNeeded = false;
605            }
606            setDisplayOrientation();
607        }
608
609        // Show the toast after getting the first orientation changed.
610        if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) {
611            mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST);
612            showTapToSnapshotToast();
613        }
614
615        // Rotate the pop-up if needed
616        if (mPopup != null) {
617            mPopup.setOrientation(mOrientationCompensation, true);
618        }
619    }
620
621    private void setOrientationIndicator(int orientation, boolean animation) {
622        Rotatable[] indicators = {
623                mBgLearningMessageRotater,
624                mReviewDoneButton, mReviewPlayButton};
625        for (Rotatable indicator : indicators) {
626            if (indicator != null) indicator.setOrientation(orientation, animation);
627        }
628        if (mGestures != null) {
629            mGestures.setOrientation(orientation);
630        }
631
632        // We change the orientation of the review cancel button only for tablet
633        // UI because there's a label along with the X icon. For phone UI, we
634        // don't change the orientation because there's only a symmetrical X
635        // icon.
636        if (mReviewCancelButton instanceof RotateLayout) {
637            mReviewCancelButton.setOrientation(orientation, animation);
638        }
639
640        // We change the orientation of the linearlayout only for phone UI because when in portrait
641        // the width is not enough.
642        if (mLabelsLinearLayout != null) {
643            if (((orientation / 90) & 1) == 0) {
644                mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL);
645            } else {
646                mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
647            }
648        }
649        mRecordingTimeRect.setOrientation(mOrientationCompensation, animation);
650    }
651
652    private void startPlayVideoActivity() {
653        Intent intent = new Intent(Intent.ACTION_VIEW);
654        intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat));
655        try {
656            mActivity.startActivity(intent);
657        } catch (ActivityNotFoundException ex) {
658            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
659        }
660    }
661
662    @OnClickAttr
663    public void onReviewPlayClicked(View v) {
664        startPlayVideoActivity();
665    }
666
667    @OnClickAttr
668    public void onReviewDoneClicked(View v) {
669        doReturnToCaller(true);
670    }
671
672    @OnClickAttr
673    public void onReviewCancelClicked(View v) {
674        stopVideoRecording();
675        doReturnToCaller(false);
676    }
677
678    private void onStopVideoRecording() {
679        mEffectsDisplayResult = true;
680        boolean recordFail = stopVideoRecording();
681        if (mIsVideoCaptureIntent) {
682            if (!effectsActive()) {
683                if (mQuickCapture) {
684                    doReturnToCaller(!recordFail);
685                } else if (!recordFail) {
686                    showAlert();
687                }
688            }
689        } else if (!recordFail){
690            // Start capture animation.
691            if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
692                // The capture animation is disabled on ICS because we use SurfaceView
693                // for preview during recording. When the recording is done, we switch
694                // back to use SurfaceTexture for preview and we need to stop then start
695                // the preview. This will cause the preview flicker since the preview
696                // will not be continuous for a short period of time.
697                ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
698            }
699        }
700    }
701
702    public void onProtectiveCurtainClick(View v) {
703        // Consume clicks
704    }
705
706    @Override
707    public void onShutterButtonClick() {
708        if (collapseCameraControls() || mSwitchingCamera) return;
709
710        boolean stop = mMediaRecorderRecording;
711
712        if (stop) {
713            onStopVideoRecording();
714        } else {
715            startVideoRecording();
716        }
717        mShutterButton.setEnabled(false);
718
719        // Keep the shutter button disabled when in video capture intent
720        // mode and recording is stopped. It'll be re-enabled when
721        // re-take button is clicked.
722        if (!(mIsVideoCaptureIntent && stop)) {
723            mHandler.sendEmptyMessageDelayed(
724                    ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
725        }
726    }
727
728    @Override
729    public void onShutterButtonFocus(boolean pressed) {
730        // Do nothing (everything happens in onShutterButtonClick).
731    }
732
733    private void readVideoPreferences() {
734        // The preference stores values from ListPreference and is thus string type for all values.
735        // We need to convert it to int manually.
736        String defaultQuality = CameraSettings.getDefaultVideoQuality(mCameraId,
737                mActivity.getResources().getString(R.string.pref_video_quality_default));
738        String videoQuality =
739                mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY,
740                        defaultQuality);
741        int quality = Integer.valueOf(videoQuality);
742
743        // Set video quality.
744        Intent intent = mActivity.getIntent();
745        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
746            int extraVideoQuality =
747                    intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
748            if (extraVideoQuality > 0) {
749                quality = CamcorderProfile.QUALITY_HIGH;
750            } else {  // 0 is mms.
751                quality = CamcorderProfile.QUALITY_LOW;
752            }
753        }
754
755        // Set video duration limit. The limit is read from the preference,
756        // unless it is specified in the intent.
757        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
758            int seconds =
759                    intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0);
760            mMaxVideoDurationInMs = 1000 * seconds;
761        } else {
762            mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity);
763        }
764
765        // Set effect
766        mEffectType = CameraSettings.readEffectType(mPreferences);
767        if (mEffectType != EffectsRecorder.EFFECT_NONE) {
768            mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
769            // Set quality to be no higher than 480p.
770            CamcorderProfile profile = CamcorderProfile.get(mCameraId, quality);
771            if (profile.videoFrameHeight > 480) {
772                quality = getLowVideoQuality();
773            }
774        } else {
775            mEffectParameter = null;
776        }
777        // Read time lapse recording interval.
778        if (ApiHelper.HAS_TIME_LAPSE_RECORDING) {
779            String frameIntervalStr = mPreferences.getString(
780                    CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
781                    mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default));
782            mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr);
783            mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0);
784        }
785        // TODO: This should be checked instead directly +1000.
786        if (mCaptureTimeLapse) quality += 1000;
787        mProfile = CamcorderProfile.get(mCameraId, quality);
788        getDesiredPreviewSize();
789    }
790
791    private void writeDefaultEffectToPrefs()  {
792        ComboPreferences.Editor editor = mPreferences.edit();
793        editor.putString(CameraSettings.KEY_VIDEO_EFFECT,
794                mActivity.getString(R.string.pref_video_effect_default));
795        editor.apply();
796    }
797
798    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
799    private void getDesiredPreviewSize() {
800        mParameters = mActivity.mCameraDevice.getParameters();
801        if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) {
802            if (mParameters.getSupportedVideoSizes() == null || effectsActive()) {
803                mDesiredPreviewWidth = mProfile.videoFrameWidth;
804                mDesiredPreviewHeight = mProfile.videoFrameHeight;
805            } else {  // Driver supports separates outputs for preview and video.
806                List<Size> sizes = mParameters.getSupportedPreviewSizes();
807                Size preferred = mParameters.getPreferredPreviewSizeForVideo();
808                int product = preferred.width * preferred.height;
809                Iterator<Size> it = sizes.iterator();
810                // Remove the preview sizes that are not preferred.
811                while (it.hasNext()) {
812                    Size size = it.next();
813                    if (size.width * size.height > product) {
814                        it.remove();
815                    }
816                }
817                Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
818                        (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
819                mDesiredPreviewWidth = optimalSize.width;
820                mDesiredPreviewHeight = optimalSize.height;
821            }
822        } else {
823            mDesiredPreviewWidth = mProfile.videoFrameWidth;
824            mDesiredPreviewHeight = mProfile.videoFrameHeight;
825        }
826        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
827                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
828    }
829
830    private void resizeForPreviewAspectRatio() {
831        mPreviewFrameLayout.setAspectRatio(
832                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
833    }
834
835    @Override
836    public void installIntentFilter() {
837        // install an intent filter to receive SD card related events.
838        IntentFilter intentFilter =
839                new IntentFilter(Intent.ACTION_MEDIA_EJECT);
840        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
841        intentFilter.addDataScheme("file");
842        mReceiver = new MyBroadcastReceiver();
843        mActivity.registerReceiver(mReceiver, intentFilter);
844    }
845
846    @Override
847    public void onResumeBeforeSuper() {
848        mPaused = false;
849    }
850
851    @Override
852    public void onResumeAfterSuper() {
853        if (mActivity.mOpenCameraFail || mActivity.mCameraDisabled)
854            return;
855
856        mZoomValue = 0;
857
858        showVideoSnapshotUI(false);
859
860
861        if (!mPreviewing) {
862            if (resetEffect()) {
863                mBgLearningMessageFrame.setVisibility(View.GONE);
864            }
865            openCamera();
866            if (mActivity.mOpenCameraFail) {
867                Util.showErrorAndFinish(mActivity,
868                        R.string.cannot_connect_camera);
869                return;
870            } else if (mActivity.mCameraDisabled) {
871                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
872                return;
873            }
874            readVideoPreferences();
875            resizeForPreviewAspectRatio();
876            startPreview();
877        }
878
879        // Initializing it here after the preview is started.
880        initializeZoom();
881
882        keepScreenOnAwhile();
883
884        // Initialize location service.
885        boolean recordLocation = RecordLocationPreference.get(mPreferences,
886                mContentResolver);
887        mLocationManager.recordLocation(recordLocation);
888
889        if (mPreviewing) {
890            mOnResumeTime = SystemClock.uptimeMillis();
891            mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
892        }
893        // Dismiss open menu if exists.
894        PopupManager.getInstance(mActivity).notifyShowPopup(null);
895
896        mVideoNamer = new VideoNamer();
897    }
898
899    private void setDisplayOrientation() {
900        mDisplayRotation = Util.getDisplayRotation(mActivity);
901        if (ApiHelper.HAS_SURFACE_TEXTURE) {
902            // The display rotation is handled by gallery.
903            mCameraDisplayOrientation = Util.getDisplayOrientation(0, mCameraId);
904        } else {
905            // We need to consider display rotation ourselves.
906            mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
907        }
908        // GLRoot also uses the DisplayRotation, and needs to be told to layout to update
909        mActivity.getGLRoot().requestLayoutContentPane();
910    }
911
912    private void startPreview() {
913        Log.v(TAG, "startPreview");
914
915        mActivity.mCameraDevice.setErrorCallback(mErrorCallback);
916        if (mPreviewing == true) {
917            stopPreview();
918            if (effectsActive() && mEffectsRecorder != null) {
919                mEffectsRecorder.release();
920                mEffectsRecorder = null;
921            }
922        }
923
924        setDisplayOrientation();
925        mActivity.mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
926        setCameraParameters();
927
928        try {
929            if (!effectsActive()) {
930                if (ApiHelper.HAS_SURFACE_TEXTURE) {
931                    mActivity.mCameraDevice.setPreviewTextureAsync(
932                            ((CameraScreenNail) mActivity.mCameraScreenNail).getSurfaceTexture());
933                } else {
934                    mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
935                }
936                mActivity.mCameraDevice.startPreviewAsync();
937            } else {
938                initializeEffectsPreview();
939                mEffectsRecorder.startPreview();
940            }
941        } catch (Throwable ex) {
942            closeCamera();
943            throw new RuntimeException("startPreview failed", ex);
944        }
945
946        mPreviewing = true;
947    }
948
949    private void stopPreview() {
950        mActivity.mCameraDevice.stopPreview();
951        mPreviewing = false;
952    }
953
954    // Closing the effects out. Will shut down the effects graph.
955    private void closeEffects() {
956        Log.v(TAG, "Closing effects");
957        mEffectType = EffectsRecorder.EFFECT_NONE;
958        if (mEffectsRecorder == null) {
959            Log.d(TAG, "Effects are already closed. Nothing to do");
960            return;
961        }
962        // This call can handle the case where the camera is already released
963        // after the recording has been stopped.
964        mEffectsRecorder.release();
965        mEffectsRecorder = null;
966    }
967
968    // By default, we want to close the effects as well with the camera.
969    private void closeCamera() {
970        closeCamera(true);
971    }
972
973    // In certain cases, when the effects are active, we may want to shutdown
974    // only the camera related parts, and handle closing the effects in the
975    // effectsUpdate callback.
976    // For example, in onPause, we want to make the camera available to
977    // outside world immediately, however, want to wait till the effects
978    // callback to shut down the effects. In such a case, we just disconnect
979    // the effects from the camera by calling disconnectCamera. That way
980    // the effects can handle that when shutting down.
981    //
982    // @param closeEffectsAlso - indicates whether we want to close the
983    // effects also along with the camera.
984    private void closeCamera(boolean closeEffectsAlso) {
985        Log.v(TAG, "closeCamera");
986        if (mActivity.mCameraDevice == null) {
987            Log.d(TAG, "already stopped.");
988            return;
989        }
990
991        if (mEffectsRecorder != null) {
992            // Disconnect the camera from effects so that camera is ready to
993            // be released to the outside world.
994            mEffectsRecorder.disconnectCamera();
995        }
996        if (closeEffectsAlso) closeEffects();
997        mActivity.mCameraDevice.setZoomChangeListener(null);
998        mActivity.mCameraDevice.setErrorCallback(null);
999        CameraHolder.instance().release();
1000        mActivity.mCameraDevice = null;
1001        mPreviewing = false;
1002        mSnapshotInProgress = false;
1003    }
1004
1005    private void releasePreviewResources() {
1006        if (ApiHelper.HAS_SURFACE_TEXTURE) {
1007            CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
1008            if (screenNail.getSurfaceTexture() != null) {
1009                screenNail.releaseSurfaceTexture();
1010            }
1011            if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
1012                mHandler.removeMessages(HIDE_SURFACE_VIEW);
1013                mPreviewSurfaceView.setVisibility(View.GONE);
1014            }
1015        }
1016    }
1017
1018    @Override
1019    public void onPauseBeforeSuper() {
1020        mPaused = true;
1021
1022        if (mMediaRecorderRecording) {
1023            // Camera will be released in onStopVideoRecording.
1024            onStopVideoRecording();
1025        } else {
1026            closeCamera();
1027            if (!effectsActive()) releaseMediaRecorder();
1028        }
1029        if (effectsActive()) {
1030            // If the effects are active, make sure we tell the graph that the
1031            // surfacetexture is not valid anymore. Disconnect the graph from
1032            // the display. This should be done before releasing the surface
1033            // texture.
1034            mEffectsRecorder.disconnectDisplay();
1035        } else {
1036            // Close the file descriptor and clear the video namer only if the
1037            // effects are not active. If effects are active, we need to wait
1038            // till we get the callback from the Effects that the graph is done
1039            // recording. That also needs a change in the stopVideoRecording()
1040            // call to not call closeCamera if the effects are active, because
1041            // that will close down the effects are well, thus making this if
1042            // condition invalid.
1043            closeVideoFileDescriptor();
1044            clearVideoNamer();
1045        }
1046
1047        releasePreviewResources();
1048
1049        if (mReceiver != null) {
1050            mActivity.unregisterReceiver(mReceiver);
1051            mReceiver = null;
1052        }
1053        resetScreenOn();
1054
1055        if (mLocationManager != null) mLocationManager.recordLocation(false);
1056
1057        mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
1058        mHandler.removeMessages(SWITCH_CAMERA);
1059        mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
1060        mPendingSwitchCameraId = -1;
1061        mSwitchingCamera = false;
1062        // Call onPause after stopping video recording. So the camera can be
1063        // released as soon as possible.
1064    }
1065
1066    @Override
1067    public void onPauseAfterSuper() {
1068    }
1069
1070    @Override
1071    public void onUserInteraction() {
1072        if (!mMediaRecorderRecording && !mActivity.isFinishing()) {
1073            keepScreenOnAwhile();
1074        }
1075    }
1076
1077    @Override
1078    public boolean onBackPressed() {
1079        if (mPaused) return true;
1080        if (mMediaRecorderRecording) {
1081            onStopVideoRecording();
1082            return true;
1083        } else if (mPieRenderer != null && mPieRenderer.showsItems()) {
1084            mPieRenderer.hide();
1085            return true;
1086        } else {
1087            return removeTopLevelPopup();
1088        }
1089    }
1090
1091    @Override
1092    public boolean onKeyDown(int keyCode, KeyEvent event) {
1093        // Do not handle any key if the activity is paused.
1094        if (mPaused) {
1095            return true;
1096        }
1097
1098        switch (keyCode) {
1099            case KeyEvent.KEYCODE_CAMERA:
1100                if (event.getRepeatCount() == 0) {
1101                    mShutterButton.performClick();
1102                    return true;
1103                }
1104                break;
1105            case KeyEvent.KEYCODE_DPAD_CENTER:
1106                if (event.getRepeatCount() == 0) {
1107                    mShutterButton.performClick();
1108                    return true;
1109                }
1110                break;
1111            case KeyEvent.KEYCODE_MENU:
1112                if (mMediaRecorderRecording) return true;
1113                break;
1114        }
1115        return false;
1116    }
1117
1118    @Override
1119    public boolean onKeyUp(int keyCode, KeyEvent event) {
1120        switch (keyCode) {
1121            case KeyEvent.KEYCODE_CAMERA:
1122                mShutterButton.setPressed(false);
1123                return true;
1124        }
1125        return false;
1126    }
1127
1128    private boolean isVideoCaptureIntent() {
1129        String action = mActivity.getIntent().getAction();
1130        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
1131    }
1132
1133    private void doReturnToCaller(boolean valid) {
1134        Intent resultIntent = new Intent();
1135        int resultCode;
1136        if (valid) {
1137            resultCode = Activity.RESULT_OK;
1138            resultIntent.setData(mCurrentVideoUri);
1139        } else {
1140            resultCode = Activity.RESULT_CANCELED;
1141        }
1142        mActivity.setResultEx(resultCode, resultIntent);
1143        mActivity.finish();
1144    }
1145
1146    private void cleanupEmptyFile() {
1147        if (mVideoFilename != null) {
1148            File f = new File(mVideoFilename);
1149            if (f.length() == 0 && f.delete()) {
1150                Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
1151                mVideoFilename = null;
1152            }
1153        }
1154    }
1155
1156    private void setupMediaRecorderPreviewDisplay() {
1157        // Nothing to do here if using SurfaceTexture.
1158        if (!ApiHelper.HAS_SURFACE_TEXTURE) {
1159            mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface());
1160        } else if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
1161            // We stop the preview here before unlocking the device because we
1162            // need to change the SurfaceTexture to SurfaceView for preview.
1163            stopPreview();
1164            mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
1165            // The orientation for SurfaceTexture is different from that for
1166            // SurfaceView. For SurfaceTexture we don't need to consider the
1167            // display rotation. Just consider the sensor's orientation and we
1168            // will set the orientation correctly when showing the texture.
1169            // Gallery will handle the orientation for the preview. For
1170            // SurfaceView we will have to take everything into account so the
1171            // display rotation is considered.
1172            mActivity.mCameraDevice.setDisplayOrientation(
1173                    Util.getDisplayOrientation(mDisplayRotation, mCameraId));
1174            mActivity.mCameraDevice.startPreviewAsync();
1175            mPreviewing = true;
1176            mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface());
1177        }
1178    }
1179
1180    // Prepares media recorder.
1181    private void initializeRecorder() {
1182        Log.v(TAG, "initializeRecorder");
1183        // If the mCameraDevice is null, then this activity is going to finish
1184        if (mActivity.mCameraDevice == null) return;
1185
1186        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING && ApiHelper.HAS_SURFACE_TEXTURE) {
1187            // Set the SurfaceView to visible so the surface gets created.
1188            // surfaceCreated() is called immediately when the visibility is
1189            // changed to visible. Thus, mSurfaceViewReady should become true
1190            // right after calling setVisibility().
1191            mPreviewSurfaceView.setVisibility(View.VISIBLE);
1192            if (!mSurfaceViewReady) return;
1193        }
1194
1195        Intent intent = mActivity.getIntent();
1196        Bundle myExtras = intent.getExtras();
1197
1198        long requestedSizeLimit = 0;
1199        closeVideoFileDescriptor();
1200        if (mIsVideoCaptureIntent && myExtras != null) {
1201            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
1202            if (saveUri != null) {
1203                try {
1204                    mVideoFileDescriptor =
1205                            mContentResolver.openFileDescriptor(saveUri, "rw");
1206                    mCurrentVideoUri = saveUri;
1207                } catch (java.io.FileNotFoundException ex) {
1208                    // invalid uri
1209                    Log.e(TAG, ex.toString());
1210                }
1211            }
1212            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
1213        }
1214        mMediaRecorder = new MediaRecorder();
1215
1216        setupMediaRecorderPreviewDisplay();
1217        // Unlock the camera object before passing it to media recorder.
1218        mActivity.mCameraDevice.unlock();
1219        mMediaRecorder.setCamera(mActivity.mCameraDevice.getCamera());
1220        if (!mCaptureTimeLapse) {
1221            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
1222        }
1223        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
1224        mMediaRecorder.setProfile(mProfile);
1225        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
1226        if (mCaptureTimeLapse) {
1227            double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs;
1228            setCaptureRate(mMediaRecorder, fps);
1229        }
1230
1231        setRecordLocation();
1232
1233        // Set output file.
1234        // Try Uri in the intent first. If it doesn't exist, use our own
1235        // instead.
1236        if (mVideoFileDescriptor != null) {
1237            mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
1238        } else {
1239            generateVideoFilename(mProfile.fileFormat);
1240            mMediaRecorder.setOutputFile(mVideoFilename);
1241        }
1242
1243        // Set maximum file size.
1244        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
1245        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
1246            maxFileSize = requestedSizeLimit;
1247        }
1248
1249        try {
1250            mMediaRecorder.setMaxFileSize(maxFileSize);
1251        } catch (RuntimeException exception) {
1252            // We are going to ignore failure of setMaxFileSize here, as
1253            // a) The composer selected may simply not support it, or
1254            // b) The underlying media framework may not handle 64-bit range
1255            // on the size restriction.
1256        }
1257
1258        // See android.hardware.Camera.Parameters.setRotation for
1259        // documentation.
1260        // Note that mOrientation here is the device orientation, which is the opposite of
1261        // what activity.getWindowManager().getDefaultDisplay().getRotation() would return,
1262        // which is the orientation the graphics need to rotate in order to render correctly.
1263        int rotation = 0;
1264        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
1265            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
1266            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
1267                rotation = (info.orientation - mOrientation + 360) % 360;
1268            } else {  // back-facing camera
1269                rotation = (info.orientation + mOrientation) % 360;
1270            }
1271        }
1272        mMediaRecorder.setOrientationHint(rotation);
1273        mOrientationCompensationAtRecordStart = mOrientationCompensation;
1274
1275        try {
1276            mMediaRecorder.prepare();
1277        } catch (IOException e) {
1278            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
1279            releaseMediaRecorder();
1280            throw new RuntimeException(e);
1281        }
1282
1283        mMediaRecorder.setOnErrorListener(this);
1284        mMediaRecorder.setOnInfoListener(this);
1285    }
1286
1287    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
1288    private static void setCaptureRate(MediaRecorder recorder, double fps) {
1289        recorder.setCaptureRate(fps);
1290    }
1291
1292    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
1293    private void setRecordLocation() {
1294        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
1295            Location loc = mLocationManager.getCurrentLocation();
1296            if (loc != null) {
1297                mMediaRecorder.setLocation((float) loc.getLatitude(),
1298                        (float) loc.getLongitude());
1299            }
1300        }
1301    }
1302
1303    private void initializeEffectsPreview() {
1304        Log.v(TAG, "initializeEffectsPreview");
1305        // If the mCameraDevice is null, then this activity is going to finish
1306        if (mActivity.mCameraDevice == null) return;
1307
1308        boolean inLandscape = (mActivity.getResources().getConfiguration().orientation
1309                == Configuration.ORIENTATION_LANDSCAPE);
1310
1311        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
1312
1313        mEffectsDisplayResult = false;
1314        mEffectsRecorder = new EffectsRecorder(mActivity);
1315
1316        // TODO: Confirm none of the following need to go to initializeEffectsRecording()
1317        // and none of these change even when the preview is not refreshed.
1318        mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation);
1319        mEffectsRecorder.setCamera(mActivity.mCameraDevice);
1320        mEffectsRecorder.setCameraFacing(info.facing);
1321        mEffectsRecorder.setProfile(mProfile);
1322        mEffectsRecorder.setEffectsListener(this);
1323        mEffectsRecorder.setOnInfoListener(this);
1324        mEffectsRecorder.setOnErrorListener(this);
1325
1326        // The input of effects recorder is affected by
1327        // android.hardware.Camera.setDisplayOrientation. Its value only
1328        // compensates the camera orientation (no Display.getRotation). So the
1329        // orientation hint here should only consider sensor orientation.
1330        int orientation = 0;
1331        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
1332            orientation = mOrientation;
1333        }
1334        mEffectsRecorder.setOrientationHint(orientation);
1335
1336        mOrientationCompensationAtRecordStart = mOrientationCompensation;
1337
1338        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
1339        mEffectsRecorder.setPreviewSurfaceTexture(screenNail.getSurfaceTexture(),
1340                screenNail.getWidth(), screenNail.getHeight());
1341
1342        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
1343                ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
1344            mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery);
1345        } else {
1346            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
1347        }
1348    }
1349
1350    private void initializeEffectsRecording() {
1351        Log.v(TAG, "initializeEffectsRecording");
1352
1353        Intent intent = mActivity.getIntent();
1354        Bundle myExtras = intent.getExtras();
1355
1356        long requestedSizeLimit = 0;
1357        closeVideoFileDescriptor();
1358        if (mIsVideoCaptureIntent && myExtras != null) {
1359            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
1360            if (saveUri != null) {
1361                try {
1362                    mVideoFileDescriptor =
1363                            mContentResolver.openFileDescriptor(saveUri, "rw");
1364                    mCurrentVideoUri = saveUri;
1365                } catch (java.io.FileNotFoundException ex) {
1366                    // invalid uri
1367                    Log.e(TAG, ex.toString());
1368                }
1369            }
1370            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
1371        }
1372
1373        mEffectsRecorder.setProfile(mProfile);
1374        // important to set the capture rate to zero if not timelapsed, since the
1375        // effectsrecorder object does not get created again for each recording
1376        // session
1377        if (mCaptureTimeLapse) {
1378            mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
1379        } else {
1380            mEffectsRecorder.setCaptureRate(0);
1381        }
1382
1383        // Set output file
1384        if (mVideoFileDescriptor != null) {
1385            mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
1386        } else {
1387            generateVideoFilename(mProfile.fileFormat);
1388            mEffectsRecorder.setOutputFile(mVideoFilename);
1389        }
1390
1391        // Set maximum file size.
1392        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
1393        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
1394            maxFileSize = requestedSizeLimit;
1395        }
1396        mEffectsRecorder.setMaxFileSize(maxFileSize);
1397        mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs);
1398    }
1399
1400
1401    private void releaseMediaRecorder() {
1402        Log.v(TAG, "Releasing media recorder.");
1403        if (mMediaRecorder != null) {
1404            cleanupEmptyFile();
1405            mMediaRecorder.reset();
1406            mMediaRecorder.release();
1407            mMediaRecorder = null;
1408        }
1409        mVideoFilename = null;
1410    }
1411
1412    private void releaseEffectsRecorder() {
1413        Log.v(TAG, "Releasing effects recorder.");
1414        if (mEffectsRecorder != null) {
1415            cleanupEmptyFile();
1416            mEffectsRecorder.release();
1417            mEffectsRecorder = null;
1418        }
1419        mEffectType = EffectsRecorder.EFFECT_NONE;
1420        mVideoFilename = null;
1421    }
1422
1423    private void generateVideoFilename(int outputFileFormat) {
1424        long dateTaken = System.currentTimeMillis();
1425        String title = createName(dateTaken);
1426        // Used when emailing.
1427        String filename = title + convertOutputFormatToFileExt(outputFileFormat);
1428        String mime = convertOutputFormatToMimeType(outputFileFormat);
1429        String path = Storage.DIRECTORY + '/' + filename;
1430        String tmpPath = path + ".tmp";
1431        mCurrentVideoValues = new ContentValues(7);
1432        mCurrentVideoValues.put(Video.Media.TITLE, title);
1433        mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename);
1434        mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken);
1435        mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime);
1436        mCurrentVideoValues.put(Video.Media.DATA, path);
1437        mCurrentVideoValues.put(Video.Media.RESOLUTION,
1438                Integer.toString(mProfile.videoFrameWidth) + "x" +
1439                Integer.toString(mProfile.videoFrameHeight));
1440        Location loc = mLocationManager.getCurrentLocation();
1441        if (loc != null) {
1442            mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude());
1443            mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude());
1444        }
1445        mVideoNamer.prepareUri(mContentResolver, mCurrentVideoValues);
1446        mVideoFilename = tmpPath;
1447        Log.v(TAG, "New video filename: " + mVideoFilename);
1448    }
1449
1450    private boolean addVideoToMediaStore() {
1451        boolean fail = false;
1452        if (mVideoFileDescriptor == null) {
1453            mCurrentVideoValues.put(Video.Media.SIZE,
1454                    new File(mCurrentVideoFilename).length());
1455            long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
1456            if (duration > 0) {
1457                if (mCaptureTimeLapse) {
1458                    duration = getTimeLapseVideoLength(duration);
1459                }
1460                mCurrentVideoValues.put(Video.Media.DURATION, duration);
1461            } else {
1462                Log.w(TAG, "Video duration <= 0 : " + duration);
1463            }
1464            try {
1465                mCurrentVideoUri = mVideoNamer.getUri();
1466                mActivity.addSecureAlbumItemIfNeeded(true, mCurrentVideoUri);
1467
1468                // Rename the video file to the final name. This avoids other
1469                // apps reading incomplete data.  We need to do it after the
1470                // above mVideoNamer.getUri() call, so we are certain that the
1471                // previous insert to MediaProvider is completed.
1472                String finalName = mCurrentVideoValues.getAsString(
1473                        Video.Media.DATA);
1474                if (new File(mCurrentVideoFilename).renameTo(new File(finalName))) {
1475                    mCurrentVideoFilename = finalName;
1476                }
1477
1478                mContentResolver.update(mCurrentVideoUri, mCurrentVideoValues
1479                        , null, null);
1480                mActivity.sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO,
1481                        mCurrentVideoUri));
1482            } catch (Exception e) {
1483                // We failed to insert into the database. This can happen if
1484                // the SD card is unmounted.
1485                Log.e(TAG, "failed to add video to media store", e);
1486                mCurrentVideoUri = null;
1487                mCurrentVideoFilename = null;
1488                fail = true;
1489            } finally {
1490                Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
1491            }
1492        }
1493        mCurrentVideoValues = null;
1494        return fail;
1495    }
1496
1497    private void deleteCurrentVideo() {
1498        // Remove the video and the uri if the uri is not passed in by intent.
1499        if (mCurrentVideoFilename != null) {
1500            deleteVideoFile(mCurrentVideoFilename);
1501            mCurrentVideoFilename = null;
1502            if (mCurrentVideoUri != null) {
1503                mContentResolver.delete(mCurrentVideoUri, null, null);
1504                mCurrentVideoUri = null;
1505            }
1506        }
1507        mActivity.updateStorageSpaceAndHint();
1508    }
1509
1510    private void deleteVideoFile(String fileName) {
1511        Log.v(TAG, "Deleting video " + fileName);
1512        File f = new File(fileName);
1513        if (!f.delete()) {
1514            Log.v(TAG, "Could not delete " + fileName);
1515        }
1516    }
1517
1518    private PreferenceGroup filterPreferenceScreenByIntent(
1519            PreferenceGroup screen) {
1520        Intent intent = mActivity.getIntent();
1521        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
1522            CameraSettings.removePreferenceFromScreen(screen,
1523                    CameraSettings.KEY_VIDEO_QUALITY);
1524        }
1525
1526        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
1527            CameraSettings.removePreferenceFromScreen(screen,
1528                    CameraSettings.KEY_VIDEO_QUALITY);
1529        }
1530        return screen;
1531    }
1532
1533    // from MediaRecorder.OnErrorListener
1534    @Override
1535    public void onError(MediaRecorder mr, int what, int extra) {
1536        Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra);
1537        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
1538            // We may have run out of space on the sdcard.
1539            stopVideoRecording();
1540            mActivity.updateStorageSpaceAndHint();
1541        }
1542    }
1543
1544    // from MediaRecorder.OnInfoListener
1545    @Override
1546    public void onInfo(MediaRecorder mr, int what, int extra) {
1547        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
1548            if (mMediaRecorderRecording) onStopVideoRecording();
1549        } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
1550            if (mMediaRecorderRecording) onStopVideoRecording();
1551
1552            // Show the toast.
1553            Toast.makeText(mActivity, R.string.video_reach_size_limit,
1554                    Toast.LENGTH_LONG).show();
1555        }
1556    }
1557
1558    /*
1559     * Make sure we're not recording music playing in the background, ask the
1560     * MediaPlaybackService to pause playback.
1561     */
1562    private void pauseAudioPlayback() {
1563        // Shamelessly copied from MediaPlaybackService.java, which
1564        // should be public, but isn't.
1565        Intent i = new Intent("com.android.music.musicservicecommand");
1566        i.putExtra("command", "pause");
1567
1568        mActivity.sendBroadcast(i);
1569    }
1570
1571    // For testing.
1572    public boolean isRecording() {
1573        return mMediaRecorderRecording;
1574    }
1575
1576    private void startVideoRecording() {
1577        Log.v(TAG, "startVideoRecording");
1578        mActivity.setSwipingEnabled(false);
1579
1580        mActivity.updateStorageSpaceAndHint();
1581        if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
1582            Log.v(TAG, "Storage issue, ignore the start request");
1583            return;
1584        }
1585
1586        mCurrentVideoUri = null;
1587        if (effectsActive()) {
1588            initializeEffectsRecording();
1589            if (mEffectsRecorder == null) {
1590                Log.e(TAG, "Fail to initialize effect recorder");
1591                return;
1592            }
1593        } else {
1594            initializeRecorder();
1595            if (mMediaRecorder == null) {
1596                Log.e(TAG, "Fail to initialize media recorder");
1597                return;
1598            }
1599        }
1600
1601        pauseAudioPlayback();
1602
1603        if (effectsActive()) {
1604            try {
1605                mEffectsRecorder.startRecording();
1606            } catch (RuntimeException e) {
1607                Log.e(TAG, "Could not start effects recorder. ", e);
1608                releaseEffectsRecorder();
1609                return;
1610            }
1611        } else {
1612            try {
1613                mMediaRecorder.start(); // Recording is now started
1614            } catch (RuntimeException e) {
1615                Log.e(TAG, "Could not start media recorder. ", e);
1616                releaseMediaRecorder();
1617                // If start fails, frameworks will not lock the camera for us.
1618                mActivity.mCameraDevice.lock();
1619                return;
1620            }
1621        }
1622
1623        // Make sure the video recording has started before announcing
1624        // this in accessibility.
1625        AccessibilityUtils.makeAnnouncement(mShutterButton,
1626                mActivity.getString(R.string.video_recording_started));
1627
1628        // The parameters may have been changed by MediaRecorder upon starting
1629        // recording. We need to alter the parameters if we support camcorder
1630        // zoom. To reduce latency when setting the parameters during zoom, we
1631        // update mParameters here once.
1632        if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) {
1633            mParameters = mActivity.mCameraDevice.getParameters();
1634        }
1635
1636        enableCameraControls(false);
1637
1638        mMediaRecorderRecording = true;
1639        mActivity.getOrientationManager().lockOrientation();
1640        mRecordingStartTime = SystemClock.uptimeMillis();
1641        showRecordingUI(true);
1642
1643        updateRecordingTime();
1644        keepScreenOn();
1645    }
1646
1647    private void showRecordingUI(boolean recording) {
1648        mMenu.setVisibility(recording ? View.GONE : View.VISIBLE);
1649        mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE);
1650        if (recording) {
1651            mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording);
1652            mActivity.hideSwitcher();
1653            mRecordingTimeView.setText("");
1654            mRecordingTimeView.setVisibility(View.VISIBLE);
1655            if (mReviewControl != null) mReviewControl.setVisibility(View.GONE);
1656            // The camera is not allowed to be accessed in older api levels during
1657            // recording. It is therefore necessary to hide the zoom UI on older
1658            // platforms.
1659            // See the documentation of android.media.MediaRecorder.start() for
1660            // further explanation.
1661            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING
1662                    && mParameters.isZoomSupported()) {
1663                // TODO: disable zoom UI here.
1664            }
1665        } else {
1666            mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
1667            mActivity.showSwitcher();
1668            mRecordingTimeView.setVisibility(View.GONE);
1669            if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE);
1670            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING
1671                    && mParameters.isZoomSupported()) {
1672                // TODO: enable zoom UI here.
1673            }
1674        }
1675    }
1676
1677    private void showAlert() {
1678        Bitmap bitmap = null;
1679        if (mVideoFileDescriptor != null) {
1680            bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(),
1681                    mPreviewFrameLayout.getWidth());
1682        } else if (mCurrentVideoFilename != null) {
1683            bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename,
1684                    mPreviewFrameLayout.getWidth());
1685        }
1686        if (bitmap != null) {
1687            // MetadataRetriever already rotates the thumbnail. We should rotate
1688            // it to match the UI orientation (and mirror if it is front-facing camera).
1689            CameraInfo[] info = CameraHolder.instance().getCameraInfo();
1690            boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
1691            bitmap = Util.rotateAndMirror(bitmap, -mOrientationCompensationAtRecordStart,
1692                    mirror);
1693            mReviewImage.setImageBitmap(bitmap);
1694            mReviewImage.setVisibility(View.VISIBLE);
1695        }
1696
1697        Util.fadeOut(mShutterButton);
1698
1699        Util.fadeIn((View) mReviewDoneButton);
1700        Util.fadeIn(mReviewPlayButton);
1701        mMenu.setVisibility(View.GONE);
1702        mOnScreenIndicators.setVisibility(View.GONE);
1703        enableCameraControls(false);
1704
1705        showTimeLapseUI(false);
1706    }
1707
1708    private void hideAlert() {
1709        mReviewImage.setVisibility(View.GONE);
1710        mShutterButton.setEnabled(true);
1711        mMenu.setVisibility(View.VISIBLE);
1712        mOnScreenIndicators.setVisibility(View.VISIBLE);
1713        enableCameraControls(true);
1714
1715        Util.fadeOut((View) mReviewDoneButton);
1716        Util.fadeOut(mReviewPlayButton);
1717
1718        Util.fadeIn(mShutterButton);
1719
1720        if (mCaptureTimeLapse) {
1721            showTimeLapseUI(true);
1722        }
1723    }
1724
1725    private boolean stopVideoRecording() {
1726        Log.v(TAG, "stopVideoRecording");
1727        mActivity.setSwipingEnabled(true);
1728        mActivity.showSwitcher();
1729
1730        boolean fail = false;
1731        if (mMediaRecorderRecording) {
1732            boolean shouldAddToMediaStoreNow = false;
1733
1734            try {
1735                if (effectsActive()) {
1736                    // This is asynchronous, so we can't add to media store now because thumbnail
1737                    // may not be ready. In such case addVideoToMediaStore is called later
1738                    // through a callback from the MediaEncoderFilter to EffectsRecorder,
1739                    // and then to the VideoModule.
1740                    mEffectsRecorder.stopRecording();
1741                } else {
1742                    mMediaRecorder.setOnErrorListener(null);
1743                    mMediaRecorder.setOnInfoListener(null);
1744                    mMediaRecorder.stop();
1745                    shouldAddToMediaStoreNow = true;
1746                }
1747                mCurrentVideoFilename = mVideoFilename;
1748                Log.v(TAG, "stopVideoRecording: Setting current video filename: "
1749                        + mCurrentVideoFilename);
1750                AccessibilityUtils.makeAnnouncement(mShutterButton,
1751                        mActivity.getString(R.string.video_recording_stopped));
1752            } catch (RuntimeException e) {
1753                Log.e(TAG, "stop fail",  e);
1754                if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
1755                fail = true;
1756            }
1757            mMediaRecorderRecording = false;
1758            mActivity.getOrientationManager().unlockOrientation();
1759
1760            // If the activity is paused, this means activity is interrupted
1761            // during recording. Release the camera as soon as possible because
1762            // face unlock or other applications may need to use the camera.
1763            // However, if the effects are active, then we can only release the
1764            // camera and cannot release the effects recorder since that will
1765            // stop the graph. It is possible to separate out the Camera release
1766            // part and the effects release part. However, the effects recorder
1767            // does hold on to the camera, hence, it needs to be "disconnected"
1768            // from the camera in the closeCamera call.
1769            if (mPaused) {
1770                // Closing only the camera part if effects active. Effects will
1771                // be closed in the callback from effects.
1772                boolean closeEffects = !effectsActive();
1773                closeCamera(closeEffects);
1774            }
1775
1776            showRecordingUI(false);
1777            if (!mIsVideoCaptureIntent) {
1778                enableCameraControls(true);
1779            }
1780            // The orientation was fixed during video recording. Now make it
1781            // reflect the device orientation as video recording is stopped.
1782            setOrientationIndicator(mOrientationCompensation, true);
1783            keepScreenOnAwhile();
1784            if (shouldAddToMediaStoreNow) {
1785                if (addVideoToMediaStore()) fail = true;
1786            }
1787        }
1788        // always release media recorder if no effects running
1789        if (!effectsActive()) {
1790            releaseMediaRecorder();
1791            if (!mPaused) {
1792                mActivity.mCameraDevice.lock();
1793                if (ApiHelper.HAS_SURFACE_TEXTURE &&
1794                    !ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
1795                    stopPreview();
1796                    // Switch back to use SurfaceTexture for preview.
1797                    ((CameraScreenNail) mActivity.mCameraScreenNail).setOneTimeOnFrameDrawnListener(
1798                            mFrameDrawnListener);
1799                    startPreview();
1800                }
1801            }
1802        }
1803        // Update the parameters here because the parameters might have been altered
1804        // by MediaRecorder.
1805        if (!mPaused) mParameters = mActivity.mCameraDevice.getParameters();
1806        return fail;
1807    }
1808
1809    private void resetScreenOn() {
1810        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1811        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1812    }
1813
1814    private void keepScreenOnAwhile() {
1815        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1816        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1817        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1818    }
1819
1820    private void keepScreenOn() {
1821        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
1822        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1823    }
1824
1825    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
1826        long seconds = milliSeconds / 1000; // round down to compute seconds
1827        long minutes = seconds / 60;
1828        long hours = minutes / 60;
1829        long remainderMinutes = minutes - (hours * 60);
1830        long remainderSeconds = seconds - (minutes * 60);
1831
1832        StringBuilder timeStringBuilder = new StringBuilder();
1833
1834        // Hours
1835        if (hours > 0) {
1836            if (hours < 10) {
1837                timeStringBuilder.append('0');
1838            }
1839            timeStringBuilder.append(hours);
1840
1841            timeStringBuilder.append(':');
1842        }
1843
1844        // Minutes
1845        if (remainderMinutes < 10) {
1846            timeStringBuilder.append('0');
1847        }
1848        timeStringBuilder.append(remainderMinutes);
1849        timeStringBuilder.append(':');
1850
1851        // Seconds
1852        if (remainderSeconds < 10) {
1853            timeStringBuilder.append('0');
1854        }
1855        timeStringBuilder.append(remainderSeconds);
1856
1857        // Centi seconds
1858        if (displayCentiSeconds) {
1859            timeStringBuilder.append('.');
1860            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
1861            if (remainderCentiSeconds < 10) {
1862                timeStringBuilder.append('0');
1863            }
1864            timeStringBuilder.append(remainderCentiSeconds);
1865        }
1866
1867        return timeStringBuilder.toString();
1868    }
1869
1870    private long getTimeLapseVideoLength(long deltaMs) {
1871        // For better approximation calculate fractional number of frames captured.
1872        // This will update the video time at a higher resolution.
1873        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
1874        return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
1875    }
1876
1877    private void updateRecordingTime() {
1878        if (!mMediaRecorderRecording) {
1879            return;
1880        }
1881        long now = SystemClock.uptimeMillis();
1882        long delta = now - mRecordingStartTime;
1883
1884        // Starting a minute before reaching the max duration
1885        // limit, we'll countdown the remaining time instead.
1886        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
1887                && delta >= mMaxVideoDurationInMs - 60000);
1888
1889        long deltaAdjusted = delta;
1890        if (countdownRemainingTime) {
1891            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
1892        }
1893        String text;
1894
1895        long targetNextUpdateDelay;
1896        if (!mCaptureTimeLapse) {
1897            text = millisecondToTimeString(deltaAdjusted, false);
1898            targetNextUpdateDelay = 1000;
1899        } else {
1900            // The length of time lapse video is different from the length
1901            // of the actual wall clock time elapsed. Display the video length
1902            // only in format hh:mm:ss.dd, where dd are the centi seconds.
1903            text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
1904            targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
1905        }
1906
1907        mRecordingTimeView.setText(text);
1908
1909        if (mRecordingTimeCountsDown != countdownRemainingTime) {
1910            // Avoid setting the color on every update, do it only
1911            // when it needs changing.
1912            mRecordingTimeCountsDown = countdownRemainingTime;
1913
1914            int color = mActivity.getResources().getColor(countdownRemainingTime
1915                    ? R.color.recording_time_remaining_text
1916                    : R.color.recording_time_elapsed_text);
1917
1918            mRecordingTimeView.setTextColor(color);
1919        }
1920
1921        long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
1922        mHandler.sendEmptyMessageDelayed(
1923                UPDATE_RECORD_TIME, actualNextUpdateDelay);
1924    }
1925
1926    private static boolean isSupported(String value, List<String> supported) {
1927        return supported == null ? false : supported.indexOf(value) >= 0;
1928    }
1929
1930    @SuppressWarnings("deprecation")
1931    private void setCameraParameters() {
1932        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
1933        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
1934
1935        // Set flash mode.
1936        String flashMode;
1937        if (mActivity.mShowCameraAppView) {
1938            flashMode = mPreferences.getString(
1939                    CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
1940                    mActivity.getString(R.string.pref_camera_video_flashmode_default));
1941        } else {
1942            flashMode = Parameters.FLASH_MODE_OFF;
1943        }
1944        List<String> supportedFlash = mParameters.getSupportedFlashModes();
1945        if (isSupported(flashMode, supportedFlash)) {
1946            mParameters.setFlashMode(flashMode);
1947        } else {
1948            flashMode = mParameters.getFlashMode();
1949            if (flashMode == null) {
1950                flashMode = mActivity.getString(
1951                        R.string.pref_camera_flashmode_no_flash);
1952            }
1953        }
1954
1955        // Set white balance parameter.
1956        String whiteBalance = mPreferences.getString(
1957                CameraSettings.KEY_WHITE_BALANCE,
1958                mActivity.getString(R.string.pref_camera_whitebalance_default));
1959        if (isSupported(whiteBalance,
1960                mParameters.getSupportedWhiteBalance())) {
1961            mParameters.setWhiteBalance(whiteBalance);
1962        } else {
1963            whiteBalance = mParameters.getWhiteBalance();
1964            if (whiteBalance == null) {
1965                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
1966            }
1967        }
1968
1969        // Set zoom.
1970        if (mParameters.isZoomSupported()) {
1971            mParameters.setZoom(mZoomValue);
1972        }
1973
1974        // Set continuous autofocus.
1975        List<String> supportedFocus = mParameters.getSupportedFocusModes();
1976        if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
1977            mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
1978        }
1979
1980        mParameters.set(Util.RECORDING_HINT, Util.TRUE);
1981
1982        // Enable video stabilization. Convenience methods not available in API
1983        // level <= 14
1984        String vstabSupported = mParameters.get("video-stabilization-supported");
1985        if ("true".equals(vstabSupported)) {
1986            mParameters.set("video-stabilization", "true");
1987        }
1988
1989        // Set picture size.
1990        // The logic here is different from the logic in still-mode camera.
1991        // There we determine the preview size based on the picture size, but
1992        // here we determine the picture size based on the preview size.
1993        List<Size> supported = mParameters.getSupportedPictureSizes();
1994        Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported,
1995                (double) mDesiredPreviewWidth / mDesiredPreviewHeight);
1996        Size original = mParameters.getPictureSize();
1997        if (!original.equals(optimalSize)) {
1998            mParameters.setPictureSize(optimalSize.width, optimalSize.height);
1999        }
2000        Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" +
2001                optimalSize.height);
2002
2003        // Set JPEG quality.
2004        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
2005                CameraProfile.QUALITY_HIGH);
2006        mParameters.setJpegQuality(jpegQuality);
2007
2008        mActivity.mCameraDevice.setParameters(mParameters);
2009        // Keep preview size up to date.
2010        mParameters = mActivity.mCameraDevice.getParameters();
2011
2012        updateCameraScreenNailSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
2013    }
2014
2015    private void updateCameraScreenNailSize(int width, int height) {
2016        if (!ApiHelper.HAS_SURFACE_TEXTURE) return;
2017
2018        if (mCameraDisplayOrientation % 180 != 0) {
2019            int tmp = width;
2020            width = height;
2021            height = tmp;
2022        }
2023
2024        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
2025        int oldWidth = screenNail.getWidth();
2026        int oldHeight = screenNail.getHeight();
2027
2028        if (oldWidth != width || oldHeight != height) {
2029            screenNail.setSize(width, height);
2030            screenNail.enableAspectRatioClamping();
2031            mActivity.notifyScreenNailChanged();
2032        }
2033
2034        if (screenNail.getSurfaceTexture() == null) {
2035            screenNail.acquireSurfaceTexture();
2036        }
2037    }
2038
2039    @Override
2040    public void onActivityResult(int requestCode, int resultCode, Intent data) {
2041        switch (requestCode) {
2042            case REQUEST_EFFECT_BACKDROPPER:
2043                if (resultCode == Activity.RESULT_OK) {
2044                    // onActivityResult() runs before onResume(), so this parameter will be
2045                    // seen by startPreview from onResume()
2046                    mEffectUriFromGallery = data.getData().toString();
2047                    Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
2048                    mResetEffect = false;
2049                } else {
2050                    mEffectUriFromGallery = null;
2051                    Log.w(TAG, "No URI from gallery");
2052                    mResetEffect = true;
2053                }
2054                break;
2055        }
2056    }
2057
2058    @Override
2059    public void onEffectsUpdate(int effectId, int effectMsg) {
2060        Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg);
2061        if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
2062            // Effects have shut down. Hide learning message if any,
2063            // and restart regular preview.
2064            mBgLearningMessageFrame.setVisibility(View.GONE);
2065            checkQualityAndStartPreview();
2066        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
2067            // This follows the codepath from onStopVideoRecording.
2068            if (mEffectsDisplayResult && !addVideoToMediaStore()) {
2069                if (mIsVideoCaptureIntent) {
2070                    if (mQuickCapture) {
2071                        doReturnToCaller(true);
2072                    } else {
2073                        showAlert();
2074                    }
2075                }
2076            }
2077            mEffectsDisplayResult = false;
2078            // In onPause, these were not called if the effects were active. We
2079            // had to wait till the effects recording is complete to do this.
2080            if (mPaused) {
2081                closeVideoFileDescriptor();
2082                clearVideoNamer();
2083            }
2084        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
2085            // Enable the shutter button once the preview is complete.
2086            mShutterButton.setEnabled(true);
2087        } else if (effectId == EffectsRecorder.EFFECT_BACKDROPPER) {
2088            switch (effectMsg) {
2089                case EffectsRecorder.EFFECT_MSG_STARTED_LEARNING:
2090                    mBgLearningMessageFrame.setVisibility(View.VISIBLE);
2091                    break;
2092                case EffectsRecorder.EFFECT_MSG_DONE_LEARNING:
2093                case EffectsRecorder.EFFECT_MSG_SWITCHING_EFFECT:
2094                    mBgLearningMessageFrame.setVisibility(View.GONE);
2095                    break;
2096            }
2097        }
2098        // In onPause, this was not called if the effects were active. We had to
2099        // wait till the effects completed to do this.
2100        if (mPaused) {
2101            Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused");
2102            closeEffects();
2103        }
2104    }
2105
2106    public void onCancelBgTraining(View v) {
2107        // Remove training message
2108        mBgLearningMessageFrame.setVisibility(View.GONE);
2109        // Write default effect out to shared prefs
2110        writeDefaultEffectToPrefs();
2111        // Tell VideoCamer to re-init based on new shared pref values.
2112        onSharedPreferenceChanged();
2113    }
2114
2115    @Override
2116    public synchronized void onEffectsError(Exception exception, String fileName) {
2117        // TODO: Eventually we may want to show the user an error dialog, and then restart the
2118        // camera and encoder gracefully. For now, we just delete the file and bail out.
2119        if (fileName != null && new File(fileName).exists()) {
2120            deleteVideoFile(fileName);
2121        }
2122        try {
2123            if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException")
2124                    .isInstance(exception)) {
2125                Log.w(TAG, "Problem recoding video file. Removing incomplete file.");
2126                return;
2127            }
2128        } catch (ClassNotFoundException ex) {
2129            Log.w(TAG, ex);
2130        }
2131        throw new RuntimeException("Error during recording!", exception);
2132    }
2133
2134    private void initializeControlByIntent() {
2135        mBlocker = mRootView.findViewById(R.id.blocker);
2136        mMenu = mRootView.findViewById(R.id.menu);
2137        mMenu.setOnClickListener(new OnClickListener() {
2138            @Override
2139            public void onClick(View v) {
2140                if (mPieRenderer != null) {
2141                    mPieRenderer.showInCenter();
2142                }
2143            }
2144        });
2145        mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators);
2146        mFlashIndicator = (ImageView) mRootView.findViewById(R.id.menu_flash_indicator);
2147        if (mIsVideoCaptureIntent) {
2148            mActivity.hideSwitcher();
2149            // Cannot use RotateImageView for "done" and "cancel" button because
2150            // the tablet layout uses RotateLayout, which cannot be cast to
2151            // RotateImageView.
2152            mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done);
2153            mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel);
2154            mReviewPlayButton = (RotateImageView) mRootView.findViewById(R.id.btn_play);
2155
2156            ((View) mReviewCancelButton).setVisibility(View.VISIBLE);
2157
2158            ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() {
2159                @Override
2160                public void onClick(View v) {
2161                    onReviewDoneClicked(v);
2162                }
2163            });
2164            ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() {
2165                @Override
2166                public void onClick(View v) {
2167                    onReviewCancelClicked(v);
2168                }
2169            });
2170
2171            ((View) mReviewPlayButton).setOnClickListener(new OnClickListener() {
2172                @Override
2173                public void onClick(View v) {
2174                    onReviewPlayClicked(v);
2175                }
2176            });
2177
2178
2179            // Not grayed out upon disabled, to make the follow-up fade-out
2180            // effect look smooth. Note that the review done button in tablet
2181            // layout is not a TwoStateImageView.
2182            if (mReviewDoneButton instanceof TwoStateImageView) {
2183                ((TwoStateImageView) mReviewDoneButton).enableFilter(false);
2184            }
2185        }
2186    }
2187
2188    private void initializeMiscControls() {
2189        mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame);
2190        mPreviewFrameLayout.setOnLayoutChangeListener(mActivity);
2191        mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image);
2192
2193        mShutterButton = mActivity.getShutterButton();
2194        mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
2195        mShutterButton.setOnShutterButtonListener(this);
2196        mShutterButton.requestFocus();
2197
2198        // Disable the shutter button if effects are ON since it might take
2199        // a little more time for the effects preview to be ready. We do not
2200        // want to allow recording before that happens. The shutter button
2201        // will be enabled when we get the message from effectsrecorder that
2202        // the preview is running. This becomes critical when the camera is
2203        // swapped.
2204        if (effectsActive()) {
2205            mShutterButton.setEnabled(false);
2206        }
2207
2208        mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time);
2209        mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect);
2210        mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label);
2211        // The R.id.labels can only be found in phone layout.
2212        // That is, mLabelsLinearLayout should be null in tablet layout.
2213        mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels);
2214
2215        mBgLearningMessageRotater = (RotateLayout) mRootView.findViewById(R.id.bg_replace_message);
2216        mBgLearningMessageFrame = mRootView.findViewById(R.id.bg_replace_message_frame);
2217    }
2218
2219    @Override
2220    public void onConfigurationChanged(Configuration newConfig) {
2221        setDisplayOrientation();
2222
2223        // Change layout in response to configuration change
2224        LayoutInflater inflater = mActivity.getLayoutInflater();
2225        ((ViewGroup) mRootView).removeAllViews();
2226        inflater.inflate(R.layout.video_module, (ViewGroup) mRootView);
2227
2228        // from onCreate()
2229        initializeControlByIntent();
2230        initializeOverlay();
2231        initializeSurfaceView();
2232        initializeMiscControls();
2233        showTimeLapseUI(mCaptureTimeLapse);
2234        initializeVideoSnapshot();
2235        resizeForPreviewAspectRatio();
2236
2237        // from onResume()
2238        showVideoSnapshotUI(false);
2239        initializeZoom();
2240        onFullScreenChanged(mActivity.isInCameraApp());
2241        updateOnScreenIndicators();
2242    }
2243
2244    @Override
2245    public void onOverriddenPreferencesClicked() {
2246    }
2247
2248    @Override
2249    // TODO: Delete this after old camera code is removed
2250    public void onRestorePreferencesClicked() {
2251    }
2252
2253    private boolean effectsActive() {
2254        return (mEffectType != EffectsRecorder.EFFECT_NONE);
2255    }
2256
2257    @Override
2258    public void onSharedPreferenceChanged() {
2259        // ignore the events after "onPause()" or preview has not started yet
2260        if (mPaused) return;
2261        synchronized (mPreferences) {
2262            // If mCameraDevice is not ready then we can set the parameter in
2263            // startPreview().
2264            if (mActivity.mCameraDevice == null) return;
2265
2266            boolean recordLocation = RecordLocationPreference.get(
2267                    mPreferences, mContentResolver);
2268            mLocationManager.recordLocation(recordLocation);
2269
2270            // Check if the current effects selection has changed
2271            if (updateEffectSelection()) return;
2272
2273            readVideoPreferences();
2274            showTimeLapseUI(mCaptureTimeLapse);
2275            // We need to restart the preview if preview size is changed.
2276            Size size = mParameters.getPreviewSize();
2277            if (size.width != mDesiredPreviewWidth
2278                    || size.height != mDesiredPreviewHeight) {
2279                if (!effectsActive()) {
2280                    stopPreview();
2281                } else {
2282                    mEffectsRecorder.release();
2283                    mEffectsRecorder = null;
2284                }
2285                resizeForPreviewAspectRatio();
2286                startPreview(); // Parameters will be set in startPreview().
2287            } else {
2288                setCameraParameters();
2289            }
2290            updateOnScreenIndicators();
2291        }
2292    }
2293
2294    private void updateOnScreenIndicators() {
2295        updateFlashOnScreenIndicator(mParameters.getFlashMode());
2296    }
2297
2298    private void updateFlashOnScreenIndicator(String value) {
2299        if (mFlashIndicator == null) {
2300            return;
2301        }
2302        if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) {
2303            mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
2304        } else {
2305            if (Parameters.FLASH_MODE_AUTO.equals(value)) {
2306                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto);
2307            } else if (Parameters.FLASH_MODE_ON.equals(value) ||
2308                    Parameters.FLASH_MODE_TORCH.equals(value)) {
2309                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on);
2310            } else {
2311                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
2312            }
2313        }
2314    }
2315
2316    private void switchCamera() {
2317        if (mPaused) return;
2318
2319        Log.d(TAG, "Start to switch camera.");
2320        mCameraId = mPendingSwitchCameraId;
2321        mPendingSwitchCameraId = -1;
2322        mVideoControl.setCameraId(mCameraId);
2323
2324        closeCamera();
2325
2326        // Restart the camera and initialize the UI. From onCreate.
2327        mPreferences.setLocalId(mActivity, mCameraId);
2328        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
2329        openCamera();
2330        readVideoPreferences();
2331        startPreview();
2332        initializeVideoSnapshot();
2333        resizeForPreviewAspectRatio();
2334        initializeVideoControl();
2335
2336        // From onResume
2337        initializeZoom();
2338        setOrientationIndicator(mOrientationCompensation, false);
2339
2340        if (ApiHelper.HAS_SURFACE_TEXTURE) {
2341            // Start switch camera animation. Post a message because
2342            // onFrameAvailable from the old camera may already exist.
2343            mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
2344        }
2345        updateOnScreenIndicators();
2346    }
2347
2348    // Preview texture has been copied. Now camera can be released and the
2349    // animation can be started.
2350    @Override
2351    public void onPreviewTextureCopied() {
2352        mHandler.sendEmptyMessage(SWITCH_CAMERA);
2353    }
2354
2355    @Override
2356    public void onCaptureTextureCopied() {
2357    }
2358
2359    private boolean updateEffectSelection() {
2360        int previousEffectType = mEffectType;
2361        Object previousEffectParameter = mEffectParameter;
2362        mEffectType = CameraSettings.readEffectType(mPreferences);
2363        mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
2364
2365        if (mEffectType == previousEffectType) {
2366            if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
2367            if (mEffectParameter.equals(previousEffectParameter)) return false;
2368        }
2369        Log.v(TAG, "New effect selection: " + mPreferences.getString(
2370                CameraSettings.KEY_VIDEO_EFFECT, "none"));
2371
2372        if (mEffectType == EffectsRecorder.EFFECT_NONE) {
2373            // Stop effects and return to normal preview
2374            mEffectsRecorder.stopPreview();
2375            mPreviewing = false;
2376            return true;
2377        }
2378        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
2379            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
2380            // Request video from gallery to use for background
2381            Intent i = new Intent(Intent.ACTION_PICK);
2382            i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
2383                             "video/*");
2384            i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
2385            mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER);
2386            return true;
2387        }
2388        if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
2389            // Stop regular preview and start effects.
2390            stopPreview();
2391            checkQualityAndStartPreview();
2392        } else {
2393            // Switch currently running effect
2394            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
2395        }
2396        return true;
2397    }
2398
2399    // Verifies that the current preview view size is correct before starting
2400    // preview. If not, resets the surface texture and resizes the view.
2401    private void checkQualityAndStartPreview() {
2402        readVideoPreferences();
2403        showTimeLapseUI(mCaptureTimeLapse);
2404        Size size = mParameters.getPreviewSize();
2405        if (size.width != mDesiredPreviewWidth
2406                || size.height != mDesiredPreviewHeight) {
2407            resizeForPreviewAspectRatio();
2408        }
2409        // Start up preview again
2410        startPreview();
2411    }
2412
2413    private void showTimeLapseUI(boolean enable) {
2414        if (mTimeLapseLabel != null) {
2415            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
2416        }
2417    }
2418
2419    @Override
2420    public boolean dispatchTouchEvent(MotionEvent m) {
2421        if (mSwitchingCamera) return true;
2422        if (mPopup == null && mGestures != null && mRenderOverlay != null) {
2423            return mGestures.dispatchTouch(m);
2424        } else if (mPopup != null) {
2425            return mActivity.superDispatchTouchEvent(m);
2426        }
2427        return false;
2428    }
2429
2430    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
2431        @Override
2432        public void onZoomValueChanged(int value) {
2433            // Not useful to change zoom value when the activity is paused.
2434            if (mPaused) return;
2435            mZoomValue = value;
2436            // Set zoom parameters asynchronously
2437            mParameters.setZoom(mZoomValue);
2438            mActivity.mCameraDevice.setParametersAsync(mParameters);
2439            Parameters p = mActivity.mCameraDevice.getParameters();
2440            mZoomRenderer.setZoomValue(mZoomRatios.get(p.getZoom()));
2441        }
2442
2443        @Override
2444        public void onZoomStart() {
2445        }
2446        @Override
2447        public void onZoomEnd() {
2448        }
2449    }
2450
2451    private void initializeZoom() {
2452        if (!mParameters.isZoomSupported()) return;
2453        mZoomMax = mParameters.getMaxZoom();
2454        mZoomRatios = mParameters.getZoomRatios();
2455        // Currently we use immediate zoom for fast zooming to get better UX and
2456        // there is no plan to take advantage of the smooth zoom.
2457        mZoomRenderer.setZoomMax(mZoomMax);
2458        mZoomRenderer.setZoom(mParameters.getZoom());
2459        mZoomRenderer.setZoomValue(mZoomRatios.get(mParameters.getZoom()));
2460        mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
2461    }
2462
2463    private void initializeVideoSnapshot() {
2464        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
2465            mActivity.setSingleTapUpListener(mPreviewFrameLayout);
2466            // Show the tap to focus toast if this is the first start.
2467            if (mPreferences.getBoolean(
2468                        CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) {
2469                // Delay the toast for one second to wait for orientation.
2470                mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000);
2471            }
2472        } else {
2473            mActivity.setSingleTapUpListener(null);
2474        }
2475    }
2476
2477    void showVideoSnapshotUI(boolean enabled) {
2478        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
2479            if (ApiHelper.HAS_SURFACE_TEXTURE && enabled) {
2480                ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
2481            } else {
2482                mPreviewFrameLayout.showBorder(enabled);
2483            }
2484            mShutterButton.setEnabled(!enabled);
2485        }
2486    }
2487
2488    // Preview area is touched. Take a picture.
2489    @Override
2490    public void onSingleTapUp(View view, int x, int y) {
2491        if (mMediaRecorderRecording && effectsActive()) {
2492            new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint,
2493                    mOrientation).show();
2494            return;
2495        }
2496
2497        if (mPaused || mSnapshotInProgress || effectsActive()) {
2498            return;
2499        }
2500
2501        if (!mMediaRecorderRecording) {
2502            // check for dismissing popup
2503            if (mPopup != null) {
2504                dismissPopup(true);
2505            }
2506            return;
2507        }
2508
2509        // Set rotation and gps data.
2510        int rotation = Util.getJpegRotation(mCameraId, mOrientation);
2511        mParameters.setRotation(rotation);
2512        Location loc = mLocationManager.getCurrentLocation();
2513        Util.setGpsParameters(mParameters, loc);
2514        mActivity.mCameraDevice.setParameters(mParameters);
2515
2516        Log.v(TAG, "Video snapshot start");
2517        mActivity.mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc));
2518        showVideoSnapshotUI(true);
2519        mSnapshotInProgress = true;
2520    }
2521
2522    @Override
2523    public void updateCameraAppView() {
2524        if (!mPreviewing || mParameters.getFlashMode() == null) return;
2525
2526        // When going to and back from gallery, we need to turn off/on the flash.
2527        if (!mActivity.mShowCameraAppView) {
2528            if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) {
2529                mRestoreFlash = false;
2530                return;
2531            }
2532            mRestoreFlash = true;
2533            setCameraParameters();
2534        } else if (mRestoreFlash) {
2535            mRestoreFlash = false;
2536            setCameraParameters();
2537        }
2538    }
2539
2540    private void setShowMenu(boolean show) {
2541        if (mOnScreenIndicators != null) {
2542            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
2543        }
2544        if (mMenu != null) {
2545            mMenu.setVisibility(show ? View.VISIBLE : View.GONE);
2546        }
2547    }
2548
2549    @Override
2550    public void onFullScreenChanged(boolean full) {
2551        if (mGestures != null) {
2552            mGestures.setEnabled(full);
2553        }
2554        if (mPopup != null) {
2555            dismissPopup(false, full);
2556        }
2557        if (mRenderOverlay != null) {
2558            // this can not happen in capture mode
2559            mRenderOverlay.setVisibility(full ? View.VISIBLE : View.GONE);
2560        }
2561        setShowMenu(full);
2562        if (mBlocker != null) {
2563            // this can not happen in capture mode
2564            mBlocker.setVisibility(full ? View.VISIBLE : View.GONE);
2565        }
2566        if (ApiHelper.HAS_SURFACE_TEXTURE) {
2567            if (mActivity.mCameraScreenNail != null) {
2568                ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full);
2569            }
2570            return;
2571        }
2572        if (full) {
2573            mPreviewSurfaceView.expand();
2574        } else {
2575            mPreviewSurfaceView.shrink();
2576        }
2577    }
2578
2579    private final class JpegPictureCallback implements PictureCallback {
2580        Location mLocation;
2581
2582        public JpegPictureCallback(Location loc) {
2583            mLocation = loc;
2584        }
2585
2586        @Override
2587        public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
2588            Log.v(TAG, "onPictureTaken");
2589            mSnapshotInProgress = false;
2590            showVideoSnapshotUI(false);
2591            storeImage(jpegData, mLocation);
2592        }
2593    }
2594
2595    private void storeImage(final byte[] data, Location loc) {
2596        long dateTaken = System.currentTimeMillis();
2597        String title = Util.createJpegName(dateTaken);
2598        int orientation = Exif.getOrientation(data);
2599        Size s = mParameters.getPictureSize();
2600        Uri uri = Storage.addImage(mContentResolver, title, dateTaken, loc, orientation, data,
2601                s.width, s.height);
2602        if (uri != null) {
2603            Util.broadcastNewPicture(mActivity, uri);
2604        }
2605    }
2606
2607    private boolean resetEffect() {
2608        if (mResetEffect) {
2609            String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT,
2610                    mPrefVideoEffectDefault);
2611            if (!mPrefVideoEffectDefault.equals(value)) {
2612                writeDefaultEffectToPrefs();
2613                return true;
2614            }
2615        }
2616        mResetEffect = true;
2617        return false;
2618    }
2619
2620    private String convertOutputFormatToMimeType(int outputFileFormat) {
2621        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
2622            return "video/mp4";
2623        }
2624        return "video/3gpp";
2625    }
2626
2627    private String convertOutputFormatToFileExt(int outputFileFormat) {
2628        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
2629            return ".mp4";
2630        }
2631        return ".3gp";
2632    }
2633
2634    private void closeVideoFileDescriptor() {
2635        if (mVideoFileDescriptor != null) {
2636            try {
2637                mVideoFileDescriptor.close();
2638            } catch (IOException e) {
2639                Log.e(TAG, "Fail to close fd", e);
2640            }
2641            mVideoFileDescriptor = null;
2642        }
2643    }
2644
2645    private void showTapToSnapshotToast() {
2646        new RotateTextToast(mActivity, R.string.video_snapshot_hint, mOrientationCompensation)
2647                .show();
2648        // Clear the preference.
2649        Editor editor = mPreferences.edit();
2650        editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false);
2651        editor.apply();
2652    }
2653
2654    private void clearVideoNamer() {
2655        if (mVideoNamer != null) {
2656            mVideoNamer.finish();
2657            mVideoNamer = null;
2658        }
2659    }
2660
2661    private static class VideoNamer extends Thread {
2662        private boolean mRequestPending;
2663        private ContentResolver mResolver;
2664        private ContentValues mValues;
2665        private boolean mStop;
2666        private Uri mUri;
2667
2668        // Runs in main thread
2669        public VideoNamer() {
2670            start();
2671        }
2672
2673        // Runs in main thread
2674        public synchronized void prepareUri(
2675                ContentResolver resolver, ContentValues values) {
2676            mRequestPending = true;
2677            mResolver = resolver;
2678            mValues = new ContentValues(values);
2679            notifyAll();
2680        }
2681
2682        // Runs in main thread
2683        public synchronized Uri getUri() {
2684            // wait until the request is done.
2685            while (mRequestPending) {
2686                try {
2687                    wait();
2688                } catch (InterruptedException ex) {
2689                    // ignore.
2690                }
2691            }
2692            Uri uri = mUri;
2693            mUri = null;
2694            return uri;
2695        }
2696
2697        // Runs in namer thread
2698        @Override
2699        public synchronized void run() {
2700            while (true) {
2701                if (mStop) break;
2702                if (!mRequestPending) {
2703                    try {
2704                        wait();
2705                    } catch (InterruptedException ex) {
2706                        // ignore.
2707                    }
2708                    continue;
2709                }
2710                cleanOldUri();
2711                generateUri();
2712                mRequestPending = false;
2713                notifyAll();
2714            }
2715            cleanOldUri();
2716        }
2717
2718        // Runs in main thread
2719        public synchronized void finish() {
2720            mStop = true;
2721            notifyAll();
2722        }
2723
2724        // Runs in namer thread
2725        private void generateUri() {
2726            Uri videoTable = Uri.parse("content://media/external/video/media");
2727            mUri = mResolver.insert(videoTable, mValues);
2728        }
2729
2730        // Runs in namer thread
2731        private void cleanOldUri() {
2732            if (mUri == null) return;
2733            mResolver.delete(mUri, null, null);
2734            mUri = null;
2735        }
2736    }
2737
2738    private class SurfaceViewCallback implements SurfaceHolder.Callback {
2739        public SurfaceViewCallback() {}
2740
2741        @Override
2742        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
2743            Log.v(TAG, "Surface changed. width=" + width + ". height=" + height);
2744        }
2745
2746        @Override
2747        public void surfaceCreated(SurfaceHolder holder) {
2748            Log.v(TAG, "Surface created");
2749            mSurfaceViewReady = true;
2750            if (mPaused) return;
2751            if (!ApiHelper.HAS_SURFACE_TEXTURE) {
2752                mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
2753                if (!mPreviewing) {
2754                    startPreview();
2755                }
2756            }
2757        }
2758
2759        @Override
2760        public void surfaceDestroyed(SurfaceHolder holder) {
2761            Log.v(TAG, "Surface destroyed");
2762            mSurfaceViewReady = false;
2763            if (mPaused) return;
2764            if (!ApiHelper.HAS_SURFACE_TEXTURE) {
2765                stopVideoRecording();
2766                stopPreview();
2767            }
2768        }
2769    }
2770
2771    @Override
2772    public boolean updateStorageHintOnResume() {
2773        return true;
2774    }
2775
2776    // required by OnPreferenceChangedListener
2777    @Override
2778    public void onCameraPickerClicked(int cameraId) {
2779        if (mPaused || mPendingSwitchCameraId != -1) return;
2780
2781        mPendingSwitchCameraId = cameraId;
2782        if (ApiHelper.HAS_SURFACE_TEXTURE) {
2783            Log.d(TAG, "Start to copy texture.");
2784            // We need to keep a preview frame for the animation before
2785            // releasing the camera. This will trigger onPreviewTextureCopied.
2786            ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
2787            // Disable all camera controls.
2788            mSwitchingCamera = true;
2789        } else {
2790            switchCamera();
2791        }
2792    }
2793
2794    @Override
2795    public boolean needsSwitcher() {
2796        return !mIsVideoCaptureIntent;
2797    }
2798
2799    @Override
2800    public void onPieOpened(int centerX, int centerY) {
2801        mActivity.cancelActivityTouchHandling();
2802        mActivity.setSwipingEnabled(false);
2803    }
2804
2805    @Override
2806    public void onPieClosed() {
2807        mActivity.setSwipingEnabled(true);
2808    }
2809
2810    public void showPopup(AbstractSettingPopup popup) {
2811        mActivity.hideUI();
2812        mBlocker.setVisibility(View.INVISIBLE);
2813        setShowMenu(false);
2814        mPopup = popup;
2815        // Make sure popup is brought up with the right orientation
2816        mPopup.setOrientation(mOrientationCompensation, false);
2817        mPopup.setVisibility(View.VISIBLE);
2818        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
2819                LayoutParams.WRAP_CONTENT);
2820        lp.gravity = Gravity.CENTER;
2821        ((FrameLayout) mRootView).addView(mPopup, lp);
2822    }
2823
2824    public void dismissPopup(boolean topLevelOnly) {
2825        dismissPopup(topLevelOnly, true);
2826    }
2827
2828    public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) {
2829        if (fullScreen) {
2830            mActivity.showUI();
2831            mBlocker.setVisibility(View.VISIBLE);
2832        }
2833        setShowMenu(fullScreen);
2834        if (mPopup != null) {
2835            ((FrameLayout) mRootView).removeView(mPopup);
2836            mPopup = null;
2837        }
2838        mVideoControl.popupDismissed(topLevelPopupOnly);
2839    }
2840
2841    @Override
2842    public void onShowSwitcherPopup() {
2843        if (mPieRenderer.showsItems()) {
2844            mPieRenderer.hide();
2845        }
2846    }
2847}
2848