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 17 package com.android.camera; 18 19 import android.annotation.TargetApi; 20 import android.app.Activity; 21 import android.content.ActivityNotFoundException; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.SharedPreferences.Editor; 29 import android.content.res.Configuration; 30 import android.graphics.Bitmap; 31 import android.graphics.SurfaceTexture; 32 import android.hardware.Camera.CameraInfo; 33 import android.hardware.Camera.Parameters; 34 import android.hardware.Camera.Size; 35 import android.location.Location; 36 import android.media.CamcorderProfile; 37 import android.media.CameraProfile; 38 import android.media.MediaRecorder; 39 import android.net.Uri; 40 import android.os.Build; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.os.Message; 44 import android.os.ParcelFileDescriptor; 45 import android.os.SystemClock; 46 import android.provider.MediaStore; 47 import android.provider.MediaStore.MediaColumns; 48 import android.provider.MediaStore.Video; 49 import android.util.Log; 50 import android.view.KeyEvent; 51 import android.view.OrientationEventListener; 52 import android.view.View; 53 import android.view.WindowManager; 54 import android.widget.Toast; 55 56 import com.android.camera.CameraManager.CameraPictureCallback; 57 import com.android.camera.CameraManager.CameraProxy; 58 import com.android.camera.app.OrientationManager; 59 import com.android.camera.exif.ExifInterface; 60 import com.android.camera.ui.RotateTextToast; 61 import com.android.camera.util.AccessibilityUtils; 62 import com.android.camera.util.ApiHelper; 63 import com.android.camera.util.CameraUtil; 64 import com.android.camera.util.UsageStatistics; 65 import com.android.camera2.R; 66 67 import java.io.File; 68 import java.io.IOException; 69 import java.text.SimpleDateFormat; 70 import java.util.Date; 71 import java.util.Iterator; 72 import java.util.List; 73 74 public class VideoModule implements CameraModule, 75 VideoController, 76 CameraPreference.OnPreferenceChangedListener, 77 ShutterButton.OnShutterButtonListener, 78 MediaRecorder.OnErrorListener, 79 MediaRecorder.OnInfoListener { 80 81 private static final String TAG = "CAM_VideoModule"; 82 83 private static final int CHECK_DISPLAY_ROTATION = 3; 84 private static final int CLEAR_SCREEN_DELAY = 4; 85 private static final int UPDATE_RECORD_TIME = 5; 86 private static final int ENABLE_SHUTTER_BUTTON = 6; 87 private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7; 88 private static final int SWITCH_CAMERA = 8; 89 private static final int SWITCH_CAMERA_START_ANIMATION = 9; 90 91 private static final int SCREEN_DELAY = 2 * 60 * 1000; 92 93 private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms 94 95 /** 96 * An unpublished intent flag requesting to start recording straight away 97 * and return as soon as recording is stopped. 98 * TODO: consider publishing by moving into MediaStore. 99 */ 100 private static final String EXTRA_QUICK_CAPTURE = 101 "android.intent.extra.quickCapture"; 102 103 // module fields 104 private CameraActivity mActivity; 105 private boolean mPaused; 106 private int mCameraId; 107 private Parameters mParameters; 108 109 private boolean mIsInReviewMode; 110 private boolean mSnapshotInProgress = false; 111 112 private final CameraErrorCallback mErrorCallback = new CameraErrorCallback(); 113 114 private ComboPreferences mPreferences; 115 private PreferenceGroup mPreferenceGroup; 116 // Preference must be read before starting preview. We check this before starting 117 // preview. 118 private boolean mPreferenceRead; 119 120 private boolean mIsVideoCaptureIntent; 121 private boolean mQuickCapture; 122 123 private MediaRecorder mMediaRecorder; 124 125 private boolean mSwitchingCamera; 126 private boolean mMediaRecorderRecording = false; 127 private long mRecordingStartTime; 128 private boolean mRecordingTimeCountsDown = false; 129 private long mOnResumeTime; 130 // The video file that the hardware camera is about to record into 131 // (or is recording into.) 132 private String mVideoFilename; 133 private ParcelFileDescriptor mVideoFileDescriptor; 134 135 // The video file that has already been recorded, and that is being 136 // examined by the user. 137 private String mCurrentVideoFilename; 138 private Uri mCurrentVideoUri; 139 private boolean mCurrentVideoUriFromMediaSaved; 140 private ContentValues mCurrentVideoValues; 141 142 private CamcorderProfile mProfile; 143 144 // The video duration limit. 0 menas no limit. 145 private int mMaxVideoDurationInMs; 146 147 // Time Lapse parameters. 148 private boolean mCaptureTimeLapse = false; 149 // Default 0. If it is larger than 0, the camcorder is in time lapse mode. 150 private int mTimeBetweenTimeLapseFrameCaptureMs = 0; 151 152 boolean mPreviewing = false; // True if preview is started. 153 // The display rotation in degrees. This is only valid when mPreviewing is 154 // true. 155 private int mDisplayRotation; 156 private int mCameraDisplayOrientation; 157 158 private int mDesiredPreviewWidth; 159 private int mDesiredPreviewHeight; 160 private ContentResolver mContentResolver; 161 162 private LocationManager mLocationManager; 163 private OrientationManager mOrientationManager; 164 165 private int mPendingSwitchCameraId; 166 private final Handler mHandler = new MainHandler(); 167 private VideoUI mUI; 168 private CameraProxy mCameraDevice; 169 170 // The degrees of the device rotated clockwise from its natural orientation. 171 private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; 172 173 private int mZoomValue; // The current zoom value. 174 175 private final MediaSaveService.OnMediaSavedListener mOnVideoSavedListener = 176 new MediaSaveService.OnMediaSavedListener() { 177 @Override 178 public void onMediaSaved(Uri uri) { 179 if (uri != null) { 180 mCurrentVideoUri = uri; 181 mCurrentVideoUriFromMediaSaved = true; 182 onVideoSaved(); 183 mActivity.notifyNewMedia(uri); 184 } 185 } 186 }; 187 188 private final MediaSaveService.OnMediaSavedListener mOnPhotoSavedListener = 189 new MediaSaveService.OnMediaSavedListener() { 190 @Override 191 public void onMediaSaved(Uri uri) { 192 if (uri != null) { 193 mActivity.notifyNewMedia(uri); 194 } 195 } 196 }; 197 198 199 protected class CameraOpenThread extends Thread { 200 @Override 201 public void run() { 202 openCamera(); 203 } 204 } 205 206 private void openCamera() { 207 if (mCameraDevice == null) { 208 mCameraDevice = CameraUtil.openCamera( 209 mActivity, mCameraId, mHandler, 210 mActivity.getCameraOpenErrorCallback()); 211 } 212 if (mCameraDevice == null) { 213 // Error. 214 return; 215 } 216 mParameters = mCameraDevice.getParameters(); 217 } 218 219 // This Handler is used to post message back onto the main thread of the 220 // application 221 private class MainHandler extends Handler { 222 @Override 223 public void handleMessage(Message msg) { 224 switch (msg.what) { 225 226 case ENABLE_SHUTTER_BUTTON: 227 mUI.enableShutter(true); 228 break; 229 230 case CLEAR_SCREEN_DELAY: { 231 mActivity.getWindow().clearFlags( 232 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 233 break; 234 } 235 236 case UPDATE_RECORD_TIME: { 237 updateRecordingTime(); 238 break; 239 } 240 241 case CHECK_DISPLAY_ROTATION: { 242 // Restart the preview if display rotation has changed. 243 // Sometimes this happens when the device is held upside 244 // down and camera app is opened. Rotation animation will 245 // take some time and the rotation value we have got may be 246 // wrong. Framework does not have a callback for this now. 247 if ((CameraUtil.getDisplayRotation(mActivity) != mDisplayRotation) 248 && !mMediaRecorderRecording && !mSwitchingCamera) { 249 startPreview(); 250 } 251 if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) { 252 mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); 253 } 254 break; 255 } 256 257 case SHOW_TAP_TO_SNAPSHOT_TOAST: { 258 showTapToSnapshotToast(); 259 break; 260 } 261 262 case SWITCH_CAMERA: { 263 switchCamera(); 264 break; 265 } 266 267 case SWITCH_CAMERA_START_ANIMATION: { 268 //TODO: 269 //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); 270 271 // Enable all camera controls. 272 mSwitchingCamera = false; 273 break; 274 } 275 276 default: 277 Log.v(TAG, "Unhandled message: " + msg.what); 278 break; 279 } 280 } 281 } 282 283 private BroadcastReceiver mReceiver = null; 284 285 private class MyBroadcastReceiver extends BroadcastReceiver { 286 @Override 287 public void onReceive(Context context, Intent intent) { 288 String action = intent.getAction(); 289 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 290 stopVideoRecording(); 291 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { 292 Toast.makeText(mActivity, 293 mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show(); 294 } 295 } 296 } 297 298 private String createName(long dateTaken) { 299 Date date = new Date(dateTaken); 300 SimpleDateFormat dateFormat = new SimpleDateFormat( 301 mActivity.getString(R.string.video_file_name_format)); 302 303 return dateFormat.format(date); 304 } 305 306 private int getPreferredCameraId(ComboPreferences preferences) { 307 int intentCameraId = CameraUtil.getCameraFacingIntentExtras(mActivity); 308 if (intentCameraId != -1) { 309 // Testing purpose. Launch a specific camera through the intent 310 // extras. 311 return intentCameraId; 312 } else { 313 return CameraSettings.readPreferredCameraId(preferences); 314 } 315 } 316 317 private void initializeSurfaceView() { 318 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { // API level < 16 319 mUI.initializeSurfaceView(); 320 } 321 } 322 323 @Override 324 public void init(CameraActivity activity, View root) { 325 mActivity = activity; 326 mUI = new VideoUI(activity, this, root); 327 mPreferences = new ComboPreferences(mActivity); 328 CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); 329 mCameraId = getPreferredCameraId(mPreferences); 330 331 mPreferences.setLocalId(mActivity, mCameraId); 332 CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); 333 334 mOrientationManager = new OrientationManager(mActivity); 335 336 /* 337 * To reduce startup time, we start the preview in another thread. 338 * We make sure the preview is started at the end of onCreate. 339 */ 340 CameraOpenThread cameraOpenThread = new CameraOpenThread(); 341 cameraOpenThread.start(); 342 343 mContentResolver = mActivity.getContentResolver(); 344 345 // Surface texture is from camera screen nail and startPreview needs it. 346 // This must be done before startPreview. 347 mIsVideoCaptureIntent = isVideoCaptureIntent(); 348 initializeSurfaceView(); 349 350 // Make sure camera device is opened. 351 try { 352 cameraOpenThread.join(); 353 if (mCameraDevice == null) { 354 return; 355 } 356 } catch (InterruptedException ex) { 357 // ignore 358 } 359 360 readVideoPreferences(); 361 mUI.setPrefChangedListener(this); 362 363 mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); 364 mLocationManager = new LocationManager(mActivity, null); 365 366 mUI.setOrientationIndicator(0, false); 367 setDisplayOrientation(); 368 369 mUI.showTimeLapseUI(mCaptureTimeLapse); 370 initializeVideoSnapshot(); 371 resizeForPreviewAspectRatio(); 372 373 initializeVideoControl(); 374 mPendingSwitchCameraId = -1; 375 } 376 377 // SingleTapListener 378 // Preview area is touched. Take a picture. 379 @Override 380 public void onSingleTapUp(View view, int x, int y) { 381 takeASnapshot(); 382 } 383 384 private void takeASnapshot() { 385 // Only take snapshots if video snapshot is supported by device 386 if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { 387 if (!mMediaRecorderRecording || mPaused || mSnapshotInProgress) { 388 return; 389 } 390 MediaSaveService s = mActivity.getMediaSaveService(); 391 if (s == null || s.isQueueFull()) { 392 return; 393 } 394 395 // Set rotation and gps data. 396 int rotation = CameraUtil.getJpegRotation(mCameraId, mOrientation); 397 mParameters.setRotation(rotation); 398 Location loc = mLocationManager.getCurrentLocation(); 399 CameraUtil.setGpsParameters(mParameters, loc); 400 mCameraDevice.setParameters(mParameters); 401 402 Log.v(TAG, "Video snapshot start"); 403 mCameraDevice.takePicture(mHandler, 404 null, null, null, new JpegPictureCallback(loc)); 405 showVideoSnapshotUI(true); 406 mSnapshotInProgress = true; 407 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 408 UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot"); 409 } 410 } 411 412 @Override 413 public void onStop() {} 414 415 private void loadCameraPreferences() { 416 CameraSettings settings = new CameraSettings(mActivity, mParameters, 417 mCameraId, CameraHolder.instance().getCameraInfo()); 418 // Remove the video quality preference setting when the quality is given in the intent. 419 mPreferenceGroup = filterPreferenceScreenByIntent( 420 settings.getPreferenceGroup(R.xml.video_preferences)); 421 } 422 423 private void initializeVideoControl() { 424 loadCameraPreferences(); 425 mUI.initializePopup(mPreferenceGroup); 426 } 427 428 @Override 429 public void onOrientationChanged(int orientation) { 430 // We keep the last known orientation. So if the user first orient 431 // the camera then point the camera to floor or sky, we still have 432 // the correct orientation. 433 if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return; 434 int newOrientation = CameraUtil.roundOrientation(orientation, mOrientation); 435 436 if (mOrientation != newOrientation) { 437 mOrientation = newOrientation; 438 } 439 440 // Show the toast after getting the first orientation changed. 441 if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) { 442 mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST); 443 showTapToSnapshotToast(); 444 } 445 } 446 447 private void startPlayVideoActivity() { 448 Intent intent = new Intent(Intent.ACTION_VIEW); 449 intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat)); 450 try { 451 mActivity 452 .startActivityForResult(intent, CameraActivity.REQ_CODE_DONT_SWITCH_TO_PREVIEW); 453 } catch (ActivityNotFoundException ex) { 454 Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); 455 } 456 } 457 458 @Override 459 @OnClickAttr 460 public void onReviewPlayClicked(View v) { 461 startPlayVideoActivity(); 462 } 463 464 @Override 465 @OnClickAttr 466 public void onReviewDoneClicked(View v) { 467 mIsInReviewMode = false; 468 doReturnToCaller(true); 469 } 470 471 @Override 472 @OnClickAttr 473 public void onReviewCancelClicked(View v) { 474 // TODO: It should be better to not even insert the URI at all before we 475 // confirm done in review, which means we need to handle temporary video 476 // files in a quite different way than we currently had. 477 // Make sure we don't delete the Uri sent from the video capture intent. 478 if (mCurrentVideoUriFromMediaSaved) { 479 mContentResolver.delete(mCurrentVideoUri, null, null); 480 } 481 mIsInReviewMode = false; 482 doReturnToCaller(false); 483 } 484 485 @Override 486 public boolean isInReviewMode() { 487 return mIsInReviewMode; 488 } 489 490 private void onStopVideoRecording() { 491 boolean recordFail = stopVideoRecording(); 492 if (mIsVideoCaptureIntent) { 493 if (mQuickCapture) { 494 doReturnToCaller(!recordFail); 495 } else if (!recordFail) { 496 showCaptureResult(); 497 } 498 } else if (!recordFail){ 499 // Start capture animation. 500 if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 501 // The capture animation is disabled on ICS because we use SurfaceView 502 // for preview during recording. When the recording is done, we switch 503 // back to use SurfaceTexture for preview and we need to stop then start 504 // the preview. This will cause the preview flicker since the preview 505 // will not be continuous for a short period of time. 506 507 mUI.animateFlash(); 508 mUI.animateCapture(); 509 } 510 } 511 } 512 513 public void onVideoSaved() { 514 if (mIsVideoCaptureIntent) { 515 showCaptureResult(); 516 } 517 } 518 519 public void onProtectiveCurtainClick(View v) { 520 // Consume clicks 521 } 522 523 @Override 524 public void onShutterButtonClick() { 525 if (mUI.collapseCameraControls() || mSwitchingCamera) return; 526 527 boolean stop = mMediaRecorderRecording; 528 529 if (stop) { 530 onStopVideoRecording(); 531 } else { 532 startVideoRecording(); 533 } 534 mUI.enableShutter(false); 535 536 // Keep the shutter button disabled when in video capture intent 537 // mode and recording is stopped. It'll be re-enabled when 538 // re-take button is clicked. 539 if (!(mIsVideoCaptureIntent && stop)) { 540 mHandler.sendEmptyMessageDelayed( 541 ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT); 542 } 543 } 544 545 @Override 546 public void onShutterButtonFocus(boolean pressed) { 547 mUI.setShutterPressed(pressed); 548 } 549 550 private void readVideoPreferences() { 551 // The preference stores values from ListPreference and is thus string type for all values. 552 // We need to convert it to int manually. 553 String videoQuality = mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY, 554 null); 555 if (videoQuality == null) { 556 // check for highest quality before setting default value 557 videoQuality = CameraSettings.getSupportedHighestVideoQuality(mCameraId, 558 mActivity.getResources().getString(R.string.pref_video_quality_default)); 559 mPreferences.edit().putString(CameraSettings.KEY_VIDEO_QUALITY, videoQuality); 560 } 561 int quality = Integer.valueOf(videoQuality); 562 563 // Set video quality. 564 Intent intent = mActivity.getIntent(); 565 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 566 int extraVideoQuality = 567 intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 568 if (extraVideoQuality > 0) { 569 quality = CamcorderProfile.QUALITY_HIGH; 570 } else { // 0 is mms. 571 quality = CamcorderProfile.QUALITY_LOW; 572 } 573 } 574 575 // Set video duration limit. The limit is read from the preference, 576 // unless it is specified in the intent. 577 if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { 578 int seconds = 579 intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0); 580 mMaxVideoDurationInMs = 1000 * seconds; 581 } else { 582 mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity); 583 } 584 585 // Read time lapse recording interval. 586 String frameIntervalStr = mPreferences.getString( 587 CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, 588 mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default)); 589 mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr); 590 mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0); 591 // TODO: This should be checked instead directly +1000. 592 if (mCaptureTimeLapse) quality += 1000; 593 mProfile = CamcorderProfile.get(mCameraId, quality); 594 getDesiredPreviewSize(); 595 mPreferenceRead = true; 596 } 597 598 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 599 private void getDesiredPreviewSize() { 600 if (mCameraDevice == null) { 601 return; 602 } 603 mParameters = mCameraDevice.getParameters(); 604 if (mParameters.getSupportedVideoSizes() == null) { 605 mDesiredPreviewWidth = mProfile.videoFrameWidth; 606 mDesiredPreviewHeight = mProfile.videoFrameHeight; 607 } else { // Driver supports separates outputs for preview and video. 608 List<Size> sizes = mParameters.getSupportedPreviewSizes(); 609 Size preferred = mParameters.getPreferredPreviewSizeForVideo(); 610 int product = preferred.width * preferred.height; 611 Iterator<Size> it = sizes.iterator(); 612 // Remove the preview sizes that are not preferred. 613 while (it.hasNext()) { 614 Size size = it.next(); 615 if (size.width * size.height > product) { 616 it.remove(); 617 } 618 } 619 Size optimalSize = CameraUtil.getOptimalPreviewSize(mActivity, sizes, 620 (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); 621 mDesiredPreviewWidth = optimalSize.width; 622 mDesiredPreviewHeight = optimalSize.height; 623 } 624 mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); 625 Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth + 626 ". mDesiredPreviewHeight=" + mDesiredPreviewHeight); 627 } 628 629 private void resizeForPreviewAspectRatio() { 630 mUI.setAspectRatio( 631 (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); 632 } 633 634 @Override 635 public void installIntentFilter() { 636 // install an intent filter to receive SD card related events. 637 IntentFilter intentFilter = 638 new IntentFilter(Intent.ACTION_MEDIA_EJECT); 639 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 640 intentFilter.addDataScheme("file"); 641 mReceiver = new MyBroadcastReceiver(); 642 mActivity.registerReceiver(mReceiver, intentFilter); 643 } 644 645 @Override 646 public void onResumeBeforeSuper() { 647 mPaused = false; 648 } 649 650 @Override 651 public void onResumeAfterSuper() { 652 mUI.enableShutter(false); 653 mZoomValue = 0; 654 655 showVideoSnapshotUI(false); 656 657 if (!mPreviewing) { 658 openCamera(); 659 if (mCameraDevice == null) { 660 return; 661 } 662 readVideoPreferences(); 663 resizeForPreviewAspectRatio(); 664 startPreview(); 665 } else { 666 // preview already started 667 mUI.enableShutter(true); 668 } 669 670 mUI.initDisplayChangeListener(); 671 // Initializing it here after the preview is started. 672 mUI.initializeZoom(mParameters); 673 674 keepScreenOnAwhile(); 675 676 mOrientationManager.resume(); 677 // Initialize location service. 678 boolean recordLocation = RecordLocationPreference.get(mPreferences, 679 mContentResolver); 680 mLocationManager.recordLocation(recordLocation); 681 682 if (mPreviewing) { 683 mOnResumeTime = SystemClock.uptimeMillis(); 684 mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); 685 } 686 687 UsageStatistics.onContentViewChanged( 688 UsageStatistics.COMPONENT_CAMERA, "VideoModule"); 689 } 690 691 private void setDisplayOrientation() { 692 mDisplayRotation = CameraUtil.getDisplayRotation(mActivity); 693 mCameraDisplayOrientation = CameraUtil.getDisplayOrientation(mDisplayRotation, mCameraId); 694 // Change the camera display orientation 695 if (mCameraDevice != null) { 696 mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); 697 } 698 } 699 700 @Override 701 public void updateCameraOrientation() { 702 if (mMediaRecorderRecording) return; 703 if (mDisplayRotation != CameraUtil.getDisplayRotation(mActivity)) { 704 setDisplayOrientation(); 705 } 706 } 707 708 @Override 709 public int onZoomChanged(int index) { 710 // Not useful to change zoom value when the activity is paused. 711 if (mPaused) return index; 712 mZoomValue = index; 713 if (mParameters == null || mCameraDevice == null) return index; 714 // Set zoom parameters asynchronously 715 mParameters.setZoom(mZoomValue); 716 mCameraDevice.setParameters(mParameters); 717 Parameters p = mCameraDevice.getParameters(); 718 if (p != null) return p.getZoom(); 719 return index; 720 } 721 722 private void startPreview() { 723 Log.v(TAG, "startPreview"); 724 725 SurfaceTexture surfaceTexture = mUI.getSurfaceTexture(); 726 if (!mPreferenceRead || surfaceTexture == null || mPaused == true || 727 mCameraDevice == null) { 728 return; 729 } 730 731 mCameraDevice.setErrorCallback(mErrorCallback); 732 if (mPreviewing == true) { 733 stopPreview(); 734 } 735 736 setDisplayOrientation(); 737 mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); 738 setCameraParameters(); 739 740 try { 741 mCameraDevice.setPreviewTexture(surfaceTexture); 742 mCameraDevice.startPreview(); 743 mPreviewing = true; 744 onPreviewStarted(); 745 } catch (Throwable ex) { 746 closeCamera(); 747 throw new RuntimeException("startPreview failed", ex); 748 } 749 } 750 751 private void onPreviewStarted() { 752 mUI.enableShutter(true); 753 } 754 755 @Override 756 public void stopPreview() { 757 if (!mPreviewing) return; 758 mCameraDevice.stopPreview(); 759 mPreviewing = false; 760 } 761 762 private void closeCamera() { 763 Log.v(TAG, "closeCamera"); 764 if (mCameraDevice == null) { 765 Log.d(TAG, "already stopped."); 766 return; 767 } 768 mCameraDevice.setZoomChangeListener(null); 769 mCameraDevice.setErrorCallback(null); 770 CameraHolder.instance().release(); 771 mCameraDevice = null; 772 mPreviewing = false; 773 mSnapshotInProgress = false; 774 } 775 776 private void releasePreviewResources() { 777 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 778 mUI.hideSurfaceView(); 779 } 780 } 781 782 @Override 783 public void onPauseBeforeSuper() { 784 mPaused = true; 785 786 mUI.showPreviewCover(); 787 if (mMediaRecorderRecording) { 788 // Camera will be released in onStopVideoRecording. 789 onStopVideoRecording(); 790 } else { 791 closeCamera(); 792 releaseMediaRecorder(); 793 } 794 795 closeVideoFileDescriptor(); 796 797 798 releasePreviewResources(); 799 800 if (mReceiver != null) { 801 mActivity.unregisterReceiver(mReceiver); 802 mReceiver = null; 803 } 804 resetScreenOn(); 805 806 if (mLocationManager != null) mLocationManager.recordLocation(false); 807 mOrientationManager.pause(); 808 809 mHandler.removeMessages(CHECK_DISPLAY_ROTATION); 810 mHandler.removeMessages(SWITCH_CAMERA); 811 mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION); 812 mPendingSwitchCameraId = -1; 813 mSwitchingCamera = false; 814 mPreferenceRead = false; 815 816 mUI.collapseCameraControls(); 817 mUI.removeDisplayChangeListener(); 818 } 819 820 @Override 821 public void onPauseAfterSuper() { 822 } 823 824 @Override 825 public void onUserInteraction() { 826 if (!mMediaRecorderRecording && !mActivity.isFinishing()) { 827 keepScreenOnAwhile(); 828 } 829 } 830 831 @Override 832 public boolean onBackPressed() { 833 if (mPaused) return true; 834 if (mMediaRecorderRecording) { 835 onStopVideoRecording(); 836 return true; 837 } else if (mUI.hidePieRenderer()) { 838 return true; 839 } else { 840 return mUI.removeTopLevelPopup(); 841 } 842 } 843 844 @Override 845 public boolean onKeyDown(int keyCode, KeyEvent event) { 846 // Do not handle any key if the activity is paused. 847 if (mPaused) { 848 return true; 849 } 850 851 switch (keyCode) { 852 case KeyEvent.KEYCODE_CAMERA: 853 if (event.getRepeatCount() == 0) { 854 mUI.clickShutter(); 855 return true; 856 } 857 break; 858 case KeyEvent.KEYCODE_DPAD_CENTER: 859 if (event.getRepeatCount() == 0) { 860 mUI.clickShutter(); 861 return true; 862 } 863 break; 864 case KeyEvent.KEYCODE_MENU: 865 if (mMediaRecorderRecording) return true; 866 break; 867 } 868 return false; 869 } 870 871 @Override 872 public boolean onKeyUp(int keyCode, KeyEvent event) { 873 switch (keyCode) { 874 case KeyEvent.KEYCODE_CAMERA: 875 mUI.pressShutter(false); 876 return true; 877 } 878 return false; 879 } 880 881 @Override 882 public boolean isVideoCaptureIntent() { 883 String action = mActivity.getIntent().getAction(); 884 return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action)); 885 } 886 887 private void doReturnToCaller(boolean valid) { 888 Intent resultIntent = new Intent(); 889 int resultCode; 890 if (valid) { 891 resultCode = Activity.RESULT_OK; 892 resultIntent.setData(mCurrentVideoUri); 893 } else { 894 resultCode = Activity.RESULT_CANCELED; 895 } 896 mActivity.setResultEx(resultCode, resultIntent); 897 mActivity.finish(); 898 } 899 900 private void cleanupEmptyFile() { 901 if (mVideoFilename != null) { 902 File f = new File(mVideoFilename); 903 if (f.length() == 0 && f.delete()) { 904 Log.v(TAG, "Empty video file deleted: " + mVideoFilename); 905 mVideoFilename = null; 906 } 907 } 908 } 909 910 private void setupMediaRecorderPreviewDisplay() { 911 // Nothing to do here if using SurfaceTexture. 912 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 913 // We stop the preview here before unlocking the device because we 914 // need to change the SurfaceTexture to SurfaceView for preview. 915 stopPreview(); 916 mCameraDevice.setPreviewDisplay(mUI.getSurfaceHolder()); 917 // The orientation for SurfaceTexture is different from that for 918 // SurfaceView. For SurfaceTexture we don't need to consider the 919 // display rotation. Just consider the sensor's orientation and we 920 // will set the orientation correctly when showing the texture. 921 // Gallery will handle the orientation for the preview. For 922 // SurfaceView we will have to take everything into account so the 923 // display rotation is considered. 924 mCameraDevice.setDisplayOrientation( 925 CameraUtil.getDisplayOrientation(mDisplayRotation, mCameraId)); 926 mCameraDevice.startPreview(); 927 mPreviewing = true; 928 mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface()); 929 } 930 } 931 932 // Prepares media recorder. 933 private void initializeRecorder() { 934 Log.v(TAG, "initializeRecorder"); 935 // If the mCameraDevice is null, then this activity is going to finish 936 if (mCameraDevice == null) return; 937 938 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 939 // Set the SurfaceView to visible so the surface gets created. 940 // surfaceCreated() is called immediately when the visibility is 941 // changed to visible. Thus, mSurfaceViewReady should become true 942 // right after calling setVisibility(). 943 mUI.showSurfaceView(); 944 } 945 946 Intent intent = mActivity.getIntent(); 947 Bundle myExtras = intent.getExtras(); 948 949 long requestedSizeLimit = 0; 950 closeVideoFileDescriptor(); 951 mCurrentVideoUriFromMediaSaved = false; 952 if (mIsVideoCaptureIntent && myExtras != null) { 953 Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); 954 if (saveUri != null) { 955 try { 956 mVideoFileDescriptor = 957 mContentResolver.openFileDescriptor(saveUri, "rw"); 958 mCurrentVideoUri = saveUri; 959 } catch (java.io.FileNotFoundException ex) { 960 // invalid uri 961 Log.e(TAG, ex.toString()); 962 } 963 } 964 requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); 965 } 966 mMediaRecorder = new MediaRecorder(); 967 968 // Unlock the camera object before passing it to media recorder. 969 mCameraDevice.unlock(); 970 mMediaRecorder.setCamera(mCameraDevice.getCamera()); 971 if (!mCaptureTimeLapse) { 972 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); 973 } 974 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 975 mMediaRecorder.setProfile(mProfile); 976 mMediaRecorder.setVideoSize(mProfile.videoFrameWidth, mProfile.videoFrameHeight); 977 mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs); 978 if (mCaptureTimeLapse) { 979 double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs; 980 setCaptureRate(mMediaRecorder, fps); 981 } 982 983 setRecordLocation(); 984 985 // Set output file. 986 // Try Uri in the intent first. If it doesn't exist, use our own 987 // instead. 988 if (mVideoFileDescriptor != null) { 989 mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); 990 } else { 991 generateVideoFilename(mProfile.fileFormat); 992 mMediaRecorder.setOutputFile(mVideoFilename); 993 } 994 995 // Set maximum file size. 996 long maxFileSize = mActivity.getStorageSpaceBytes() - Storage.LOW_STORAGE_THRESHOLD_BYTES; 997 if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { 998 maxFileSize = requestedSizeLimit; 999 } 1000 1001 try { 1002 mMediaRecorder.setMaxFileSize(maxFileSize); 1003 } catch (RuntimeException exception) { 1004 // We are going to ignore failure of setMaxFileSize here, as 1005 // a) The composer selected may simply not support it, or 1006 // b) The underlying media framework may not handle 64-bit range 1007 // on the size restriction. 1008 } 1009 1010 // See android.hardware.Camera.Parameters.setRotation for 1011 // documentation. 1012 // Note that mOrientation here is the device orientation, which is the opposite of 1013 // what activity.getWindowManager().getDefaultDisplay().getRotation() would return, 1014 // which is the orientation the graphics need to rotate in order to render correctly. 1015 int rotation = 0; 1016 if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { 1017 CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; 1018 if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { 1019 rotation = (info.orientation - mOrientation + 360) % 360; 1020 } else { // back-facing camera 1021 rotation = (info.orientation + mOrientation) % 360; 1022 } 1023 } 1024 mMediaRecorder.setOrientationHint(rotation); 1025 setupMediaRecorderPreviewDisplay(); 1026 1027 try { 1028 mMediaRecorder.prepare(); 1029 } catch (IOException e) { 1030 Log.e(TAG, "prepare failed for " + mVideoFilename, e); 1031 releaseMediaRecorder(); 1032 throw new RuntimeException(e); 1033 } 1034 1035 mMediaRecorder.setOnErrorListener(this); 1036 mMediaRecorder.setOnInfoListener(this); 1037 } 1038 1039 private static void setCaptureRate(MediaRecorder recorder, double fps) { 1040 recorder.setCaptureRate(fps); 1041 } 1042 1043 private void setRecordLocation() { 1044 Location loc = mLocationManager.getCurrentLocation(); 1045 if (loc != null) { 1046 mMediaRecorder.setLocation((float) loc.getLatitude(), 1047 (float) loc.getLongitude()); 1048 } 1049 } 1050 1051 private void releaseMediaRecorder() { 1052 Log.v(TAG, "Releasing media recorder."); 1053 if (mMediaRecorder != null) { 1054 cleanupEmptyFile(); 1055 mMediaRecorder.reset(); 1056 mMediaRecorder.release(); 1057 mMediaRecorder = null; 1058 } 1059 mVideoFilename = null; 1060 } 1061 1062 private void generateVideoFilename(int outputFileFormat) { 1063 long dateTaken = System.currentTimeMillis(); 1064 String title = createName(dateTaken); 1065 // Used when emailing. 1066 String filename = title + convertOutputFormatToFileExt(outputFileFormat); 1067 String mime = convertOutputFormatToMimeType(outputFileFormat); 1068 String path = Storage.DIRECTORY + '/' + filename; 1069 String tmpPath = path + ".tmp"; 1070 mCurrentVideoValues = new ContentValues(9); 1071 mCurrentVideoValues.put(Video.Media.TITLE, title); 1072 mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename); 1073 mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken); 1074 mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000); 1075 mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime); 1076 mCurrentVideoValues.put(Video.Media.DATA, path); 1077 mCurrentVideoValues.put(Video.Media.RESOLUTION, 1078 Integer.toString(mProfile.videoFrameWidth) + "x" + 1079 Integer.toString(mProfile.videoFrameHeight)); 1080 Location loc = mLocationManager.getCurrentLocation(); 1081 if (loc != null) { 1082 mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude()); 1083 mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude()); 1084 } 1085 mVideoFilename = tmpPath; 1086 Log.v(TAG, "New video filename: " + mVideoFilename); 1087 } 1088 1089 private void saveVideo() { 1090 if (mVideoFileDescriptor == null) { 1091 long duration = SystemClock.uptimeMillis() - mRecordingStartTime; 1092 if (duration > 0) { 1093 if (mCaptureTimeLapse) { 1094 duration = getTimeLapseVideoLength(duration); 1095 } 1096 } else { 1097 Log.w(TAG, "Video duration <= 0 : " + duration); 1098 } 1099 mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename, 1100 duration, mCurrentVideoValues, 1101 mOnVideoSavedListener, mContentResolver); 1102 } 1103 mCurrentVideoValues = null; 1104 } 1105 1106 private void deleteVideoFile(String fileName) { 1107 Log.v(TAG, "Deleting video " + fileName); 1108 File f = new File(fileName); 1109 if (!f.delete()) { 1110 Log.v(TAG, "Could not delete " + fileName); 1111 } 1112 } 1113 1114 private PreferenceGroup filterPreferenceScreenByIntent( 1115 PreferenceGroup screen) { 1116 Intent intent = mActivity.getIntent(); 1117 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 1118 CameraSettings.removePreferenceFromScreen(screen, 1119 CameraSettings.KEY_VIDEO_QUALITY); 1120 } 1121 1122 if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { 1123 CameraSettings.removePreferenceFromScreen(screen, 1124 CameraSettings.KEY_VIDEO_QUALITY); 1125 } 1126 return screen; 1127 } 1128 1129 // from MediaRecorder.OnErrorListener 1130 @Override 1131 public void onError(MediaRecorder mr, int what, int extra) { 1132 Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra); 1133 if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { 1134 // We may have run out of space on the sdcard. 1135 stopVideoRecording(); 1136 mActivity.updateStorageSpaceAndHint(); 1137 } 1138 } 1139 1140 // from MediaRecorder.OnInfoListener 1141 @Override 1142 public void onInfo(MediaRecorder mr, int what, int extra) { 1143 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { 1144 if (mMediaRecorderRecording) onStopVideoRecording(); 1145 } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 1146 if (mMediaRecorderRecording) onStopVideoRecording(); 1147 1148 // Show the toast. 1149 Toast.makeText(mActivity, R.string.video_reach_size_limit, 1150 Toast.LENGTH_LONG).show(); 1151 } 1152 } 1153 1154 /* 1155 * Make sure we're not recording music playing in the background, ask the 1156 * MediaPlaybackService to pause playback. 1157 */ 1158 private void pauseAudioPlayback() { 1159 // Shamelessly copied from MediaPlaybackService.java, which 1160 // should be public, but isn't. 1161 Intent i = new Intent("com.android.music.musicservicecommand"); 1162 i.putExtra("command", "pause"); 1163 1164 mActivity.sendBroadcast(i); 1165 } 1166 1167 // For testing. 1168 public boolean isRecording() { 1169 return mMediaRecorderRecording; 1170 } 1171 1172 private void startVideoRecording() { 1173 Log.v(TAG, "startVideoRecording"); 1174 mUI.cancelAnimations(); 1175 mUI.setSwipingEnabled(false); 1176 1177 mActivity.updateStorageSpaceAndHint(); 1178 if (mActivity.getStorageSpaceBytes() <= Storage.LOW_STORAGE_THRESHOLD_BYTES) { 1179 Log.v(TAG, "Storage issue, ignore the start request"); 1180 return; 1181 } 1182 1183 //?? 1184 //if (!mCameraDevice.waitDone()) return; 1185 mCurrentVideoUri = null; 1186 1187 initializeRecorder(); 1188 if (mMediaRecorder == null) { 1189 Log.e(TAG, "Fail to initialize media recorder"); 1190 return; 1191 } 1192 1193 pauseAudioPlayback(); 1194 1195 try { 1196 mMediaRecorder.start(); // Recording is now started 1197 } catch (RuntimeException e) { 1198 Log.e(TAG, "Could not start media recorder. ", e); 1199 releaseMediaRecorder(); 1200 // If start fails, frameworks will not lock the camera for us. 1201 mCameraDevice.lock(); 1202 return; 1203 } 1204 1205 // Make sure the video recording has started before announcing 1206 // this in accessibility. 1207 AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), 1208 mActivity.getString(R.string.video_recording_started)); 1209 1210 // The parameters might have been altered by MediaRecorder already. 1211 // We need to force mCameraDevice to refresh before getting it. 1212 mCameraDevice.refreshParameters(); 1213 // The parameters may have been changed by MediaRecorder upon starting 1214 // recording. We need to alter the parameters if we support camcorder 1215 // zoom. To reduce latency when setting the parameters during zoom, we 1216 // update mParameters here once. 1217 mParameters = mCameraDevice.getParameters(); 1218 1219 mUI.enableCameraControls(false); 1220 1221 mMediaRecorderRecording = true; 1222 mOrientationManager.lockOrientation(); 1223 mRecordingStartTime = SystemClock.uptimeMillis(); 1224 mUI.showRecordingUI(true); 1225 1226 updateRecordingTime(); 1227 keepScreenOn(); 1228 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 1229 UsageStatistics.ACTION_CAPTURE_START, "Video"); 1230 } 1231 1232 private Bitmap getVideoThumbnail() { 1233 Bitmap bitmap = null; 1234 if (mVideoFileDescriptor != null) { 1235 bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(), 1236 mDesiredPreviewWidth); 1237 } else if (mCurrentVideoUri != null) { 1238 try { 1239 mVideoFileDescriptor = mContentResolver.openFileDescriptor(mCurrentVideoUri, "r"); 1240 bitmap = Thumbnail.createVideoThumbnailBitmap( 1241 mVideoFileDescriptor.getFileDescriptor(), mDesiredPreviewWidth); 1242 } catch (java.io.FileNotFoundException ex) { 1243 // invalid uri 1244 Log.e(TAG, ex.toString()); 1245 } 1246 } 1247 1248 if (bitmap != null) { 1249 // MetadataRetriever already rotates the thumbnail. We should rotate 1250 // it to match the UI orientation (and mirror if it is front-facing camera). 1251 CameraInfo[] info = CameraHolder.instance().getCameraInfo(); 1252 boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT); 1253 bitmap = CameraUtil.rotateAndMirror(bitmap, 0, mirror); 1254 } 1255 return bitmap; 1256 } 1257 1258 private void showCaptureResult() { 1259 mIsInReviewMode = true; 1260 Bitmap bitmap = getVideoThumbnail(); 1261 if (bitmap != null) { 1262 mUI.showReviewImage(bitmap); 1263 } 1264 mUI.showReviewControls(); 1265 mUI.enableCameraControls(false); 1266 mUI.showTimeLapseUI(false); 1267 } 1268 1269 private boolean stopVideoRecording() { 1270 Log.v(TAG, "stopVideoRecording"); 1271 mUI.setSwipingEnabled(true); 1272 if (!isVideoCaptureIntent()) { 1273 mUI.showSwitcher(); 1274 } 1275 1276 boolean fail = false; 1277 if (mMediaRecorderRecording) { 1278 boolean shouldAddToMediaStoreNow = false; 1279 1280 try { 1281 mMediaRecorder.setOnErrorListener(null); 1282 mMediaRecorder.setOnInfoListener(null); 1283 mMediaRecorder.stop(); 1284 shouldAddToMediaStoreNow = true; 1285 mCurrentVideoFilename = mVideoFilename; 1286 Log.v(TAG, "stopVideoRecording: Setting current video filename: " 1287 + mCurrentVideoFilename); 1288 AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), 1289 mActivity.getString(R.string.video_recording_stopped)); 1290 } catch (RuntimeException e) { 1291 Log.e(TAG, "stop fail", e); 1292 if (mVideoFilename != null) deleteVideoFile(mVideoFilename); 1293 fail = true; 1294 } 1295 mMediaRecorderRecording = false; 1296 mOrientationManager.unlockOrientation(); 1297 1298 // If the activity is paused, this means activity is interrupted 1299 // during recording. Release the camera as soon as possible because 1300 // face unlock or other applications may need to use the camera. 1301 if (mPaused) { 1302 closeCamera(); 1303 } 1304 1305 mUI.showRecordingUI(false); 1306 if (!mIsVideoCaptureIntent) { 1307 mUI.enableCameraControls(true); 1308 } 1309 // The orientation was fixed during video recording. Now make it 1310 // reflect the device orientation as video recording is stopped. 1311 mUI.setOrientationIndicator(0, true); 1312 keepScreenOnAwhile(); 1313 if (shouldAddToMediaStoreNow && !fail) { 1314 if (mVideoFileDescriptor == null) { 1315 saveVideo(); 1316 } else if (mIsVideoCaptureIntent) { 1317 // if no file save is needed, we can show the post capture UI now 1318 showCaptureResult(); 1319 } 1320 } 1321 } 1322 // release media recorder 1323 releaseMediaRecorder(); 1324 if (!mPaused) { 1325 mCameraDevice.lock(); 1326 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 1327 stopPreview(); 1328 mUI.hideSurfaceView(); 1329 // Switch back to use SurfaceTexture for preview. 1330 startPreview(); 1331 } 1332 } 1333 // Update the parameters here because the parameters might have been altered 1334 // by MediaRecorder. 1335 if (!mPaused) mParameters = mCameraDevice.getParameters(); 1336 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 1337 fail ? UsageStatistics.ACTION_CAPTURE_FAIL : 1338 UsageStatistics.ACTION_CAPTURE_DONE, "Video", 1339 SystemClock.uptimeMillis() - mRecordingStartTime); 1340 return fail; 1341 } 1342 1343 private void resetScreenOn() { 1344 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1345 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1346 } 1347 1348 private void keepScreenOnAwhile() { 1349 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1350 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1351 mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1352 } 1353 1354 private void keepScreenOn() { 1355 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1356 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1357 } 1358 1359 private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) { 1360 long seconds = milliSeconds / 1000; // round down to compute seconds 1361 long minutes = seconds / 60; 1362 long hours = minutes / 60; 1363 long remainderMinutes = minutes - (hours * 60); 1364 long remainderSeconds = seconds - (minutes * 60); 1365 1366 StringBuilder timeStringBuilder = new StringBuilder(); 1367 1368 // Hours 1369 if (hours > 0) { 1370 if (hours < 10) { 1371 timeStringBuilder.append('0'); 1372 } 1373 timeStringBuilder.append(hours); 1374 1375 timeStringBuilder.append(':'); 1376 } 1377 1378 // Minutes 1379 if (remainderMinutes < 10) { 1380 timeStringBuilder.append('0'); 1381 } 1382 timeStringBuilder.append(remainderMinutes); 1383 timeStringBuilder.append(':'); 1384 1385 // Seconds 1386 if (remainderSeconds < 10) { 1387 timeStringBuilder.append('0'); 1388 } 1389 timeStringBuilder.append(remainderSeconds); 1390 1391 // Centi seconds 1392 if (displayCentiSeconds) { 1393 timeStringBuilder.append('.'); 1394 long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10; 1395 if (remainderCentiSeconds < 10) { 1396 timeStringBuilder.append('0'); 1397 } 1398 timeStringBuilder.append(remainderCentiSeconds); 1399 } 1400 1401 return timeStringBuilder.toString(); 1402 } 1403 1404 private long getTimeLapseVideoLength(long deltaMs) { 1405 // For better approximation calculate fractional number of frames captured. 1406 // This will update the video time at a higher resolution. 1407 double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs; 1408 return (long) (numberOfFrames / mProfile.videoFrameRate * 1000); 1409 } 1410 1411 private void updateRecordingTime() { 1412 if (!mMediaRecorderRecording) { 1413 return; 1414 } 1415 long now = SystemClock.uptimeMillis(); 1416 long delta = now - mRecordingStartTime; 1417 1418 // Starting a minute before reaching the max duration 1419 // limit, we'll countdown the remaining time instead. 1420 boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0 1421 && delta >= mMaxVideoDurationInMs - 60000); 1422 1423 long deltaAdjusted = delta; 1424 if (countdownRemainingTime) { 1425 deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999; 1426 } 1427 String text; 1428 1429 long targetNextUpdateDelay; 1430 if (!mCaptureTimeLapse) { 1431 text = millisecondToTimeString(deltaAdjusted, false); 1432 targetNextUpdateDelay = 1000; 1433 } else { 1434 // The length of time lapse video is different from the length 1435 // of the actual wall clock time elapsed. Display the video length 1436 // only in format hh:mm:ss.dd, where dd are the centi seconds. 1437 text = millisecondToTimeString(getTimeLapseVideoLength(delta), true); 1438 targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs; 1439 } 1440 1441 mUI.setRecordingTime(text); 1442 1443 if (mRecordingTimeCountsDown != countdownRemainingTime) { 1444 // Avoid setting the color on every update, do it only 1445 // when it needs changing. 1446 mRecordingTimeCountsDown = countdownRemainingTime; 1447 1448 int color = mActivity.getResources().getColor(countdownRemainingTime 1449 ? R.color.recording_time_remaining_text 1450 : R.color.recording_time_elapsed_text); 1451 1452 mUI.setRecordingTimeTextColor(color); 1453 } 1454 1455 long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay); 1456 mHandler.sendEmptyMessageDelayed( 1457 UPDATE_RECORD_TIME, actualNextUpdateDelay); 1458 } 1459 1460 private static boolean isSupported(String value, List<String> supported) { 1461 return supported == null ? false : supported.indexOf(value) >= 0; 1462 } 1463 1464 @SuppressWarnings("deprecation") 1465 private void setCameraParameters() { 1466 mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); 1467 mParameters.set("video-size", mProfile.videoFrameWidth+"x"+mProfile.videoFrameHeight); 1468 int[] fpsRange = CameraUtil.getMaxPreviewFpsRange(mParameters); 1469 if (fpsRange.length > 0) { 1470 mParameters.setPreviewFpsRange( 1471 fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], 1472 fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); 1473 } else { 1474 mParameters.setPreviewFrameRate(mProfile.videoFrameRate); 1475 } 1476 1477 forceFlashOffIfSupported(!mUI.isVisible()); 1478 1479 // Set white balance parameter. 1480 String whiteBalance = mPreferences.getString( 1481 CameraSettings.KEY_WHITE_BALANCE, 1482 mActivity.getString(R.string.pref_camera_whitebalance_default)); 1483 if (isSupported(whiteBalance, 1484 mParameters.getSupportedWhiteBalance())) { 1485 mParameters.setWhiteBalance(whiteBalance); 1486 } else { 1487 whiteBalance = mParameters.getWhiteBalance(); 1488 if (whiteBalance == null) { 1489 whiteBalance = Parameters.WHITE_BALANCE_AUTO; 1490 } 1491 } 1492 1493 // Set zoom. 1494 if (mParameters.isZoomSupported()) { 1495 mParameters.setZoom(mZoomValue); 1496 } 1497 1498 // Set continuous autofocus. 1499 List<String> supportedFocus = mParameters.getSupportedFocusModes(); 1500 if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) { 1501 mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); 1502 } 1503 1504 mParameters.set(CameraUtil.RECORDING_HINT, CameraUtil.TRUE); 1505 1506 // Enable video stabilization. Convenience methods not available in API 1507 // level <= 14 1508 String vstabSupported = mParameters.get("video-stabilization-supported"); 1509 if ("true".equals(vstabSupported)) { 1510 mParameters.set("video-stabilization", "true"); 1511 } 1512 1513 // Set picture size. 1514 // The logic here is different from the logic in still-mode camera. 1515 // There we determine the preview size based on the picture size, but 1516 // here we determine the picture size based on the preview size. 1517 List<Size> supported = mParameters.getSupportedPictureSizes(); 1518 Size optimalSize = CameraUtil.getOptimalVideoSnapshotPictureSize(supported, 1519 (double) mDesiredPreviewWidth / mDesiredPreviewHeight); 1520 Size original = mParameters.getPictureSize(); 1521 if (!original.equals(optimalSize)) { 1522 mParameters.setPictureSize(optimalSize.width, optimalSize.height); 1523 } 1524 Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" + 1525 optimalSize.height); 1526 1527 // Set JPEG quality. 1528 int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, 1529 CameraProfile.QUALITY_HIGH); 1530 mParameters.setJpegQuality(jpegQuality); 1531 1532 boolean flag = false; 1533 if (mPreviewing) { 1534 stopPreview(); 1535 flag = true; 1536 } 1537 mCameraDevice.setParameters(mParameters); 1538 if (flag) { 1539 startPreview(); 1540 } 1541 // Keep preview size up to date. 1542 mParameters = mCameraDevice.getParameters(); 1543 1544 // Update UI based on the new parameters. 1545 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1546 } 1547 1548 @Override 1549 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1550 // Do nothing. 1551 } 1552 1553 @Override 1554 public void onConfigurationChanged(Configuration newConfig) { 1555 Log.v(TAG, "onConfigurationChanged"); 1556 setDisplayOrientation(); 1557 } 1558 1559 @Override 1560 public void onOverriddenPreferencesClicked() { 1561 } 1562 1563 @Override 1564 // TODO: Delete this after old camera code is removed 1565 public void onRestorePreferencesClicked() { 1566 } 1567 1568 @Override 1569 public void onSharedPreferenceChanged() { 1570 // ignore the events after "onPause()" or preview has not started yet 1571 if (mPaused) { 1572 return; 1573 } 1574 synchronized (mPreferences) { 1575 // If mCameraDevice is not ready then we can set the parameter in 1576 // startPreview(). 1577 if (mCameraDevice == null) return; 1578 1579 boolean recordLocation = RecordLocationPreference.get( 1580 mPreferences, mContentResolver); 1581 mLocationManager.recordLocation(recordLocation); 1582 1583 readVideoPreferences(); 1584 mUI.showTimeLapseUI(mCaptureTimeLapse); 1585 // We need to restart the preview if preview size is changed. 1586 Size size = mParameters.getPreviewSize(); 1587 if (size.width != mDesiredPreviewWidth 1588 || size.height != mDesiredPreviewHeight) { 1589 1590 stopPreview(); 1591 resizeForPreviewAspectRatio(); 1592 startPreview(); // Parameters will be set in startPreview(). 1593 } else { 1594 setCameraParameters(); 1595 } 1596 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1597 } 1598 } 1599 1600 protected void setCameraId(int cameraId) { 1601 ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); 1602 pref.setValue("" + cameraId); 1603 } 1604 1605 private void switchCamera() { 1606 if (mPaused) { 1607 return; 1608 } 1609 1610 Log.d(TAG, "Start to switch camera."); 1611 mCameraId = mPendingSwitchCameraId; 1612 mPendingSwitchCameraId = -1; 1613 setCameraId(mCameraId); 1614 1615 closeCamera(); 1616 mUI.collapseCameraControls(); 1617 // Restart the camera and initialize the UI. From onCreate. 1618 mPreferences.setLocalId(mActivity, mCameraId); 1619 CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); 1620 openCamera(); 1621 readVideoPreferences(); 1622 startPreview(); 1623 initializeVideoSnapshot(); 1624 resizeForPreviewAspectRatio(); 1625 initializeVideoControl(); 1626 1627 // From onResume 1628 mZoomValue = 0; 1629 mUI.initializeZoom(mParameters); 1630 mUI.setOrientationIndicator(0, false); 1631 1632 // Start switch camera animation. Post a message because 1633 // onFrameAvailable from the old camera may already exist. 1634 mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); 1635 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1636 } 1637 1638 // Preview texture has been copied. Now camera can be released and the 1639 // animation can be started. 1640 @Override 1641 public void onPreviewTextureCopied() { 1642 mHandler.sendEmptyMessage(SWITCH_CAMERA); 1643 } 1644 1645 @Override 1646 public void onCaptureTextureCopied() { 1647 } 1648 1649 private void initializeVideoSnapshot() { 1650 if (mParameters == null) return; 1651 if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { 1652 // Show the tap to focus toast if this is the first start. 1653 if (mPreferences.getBoolean( 1654 CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) { 1655 // Delay the toast for one second to wait for orientation. 1656 mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000); 1657 } 1658 } 1659 } 1660 1661 void showVideoSnapshotUI(boolean enabled) { 1662 if (mParameters == null) return; 1663 if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { 1664 if (enabled) { 1665 mUI.animateFlash(); 1666 mUI.animateCapture(); 1667 } else { 1668 mUI.showPreviewBorder(enabled); 1669 } 1670 mUI.enableShutter(!enabled); 1671 } 1672 } 1673 1674 private void forceFlashOffIfSupported(boolean forceOff) { 1675 String flashMode; 1676 if (!forceOff) { 1677 flashMode = mPreferences.getString( 1678 CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, 1679 mActivity.getString(R.string.pref_camera_video_flashmode_default)); 1680 } else { 1681 flashMode = Parameters.FLASH_MODE_OFF; 1682 } 1683 List<String> supportedFlash = mParameters.getSupportedFlashModes(); 1684 if (isSupported(flashMode, supportedFlash)) { 1685 mParameters.setFlashMode(flashMode); 1686 } else { 1687 flashMode = mParameters.getFlashMode(); 1688 if (flashMode == null) { 1689 flashMode = mActivity.getString( 1690 R.string.pref_camera_flashmode_no_flash); 1691 } 1692 } 1693 } 1694 1695 /** 1696 * Used to update the flash mode. Video mode can turn on the flash as torch 1697 * mode, which we would like to turn on and off when we switching in and 1698 * out to the preview. 1699 * 1700 * @param forceOff whether we want to force the flash off. 1701 */ 1702 private void forceFlashOff(boolean forceOff) { 1703 if (!mPreviewing || mParameters.getFlashMode() == null) { 1704 return; 1705 } 1706 forceFlashOffIfSupported(forceOff); 1707 mCameraDevice.setParameters(mParameters); 1708 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1709 } 1710 1711 @Override 1712 public void onPreviewFocusChanged(boolean previewFocused) { 1713 mUI.onPreviewFocusChanged(previewFocused); 1714 forceFlashOff(!previewFocused); 1715 } 1716 1717 @Override 1718 public boolean arePreviewControlsVisible() { 1719 return mUI.arePreviewControlsVisible(); 1720 } 1721 1722 private final class JpegPictureCallback implements CameraPictureCallback { 1723 Location mLocation; 1724 1725 public JpegPictureCallback(Location loc) { 1726 mLocation = loc; 1727 } 1728 1729 @Override 1730 public void onPictureTaken(byte [] jpegData, CameraProxy camera) { 1731 Log.v(TAG, "onPictureTaken"); 1732 mSnapshotInProgress = false; 1733 showVideoSnapshotUI(false); 1734 storeImage(jpegData, mLocation); 1735 } 1736 } 1737 1738 private void storeImage(final byte[] data, Location loc) { 1739 long dateTaken = System.currentTimeMillis(); 1740 String title = CameraUtil.createJpegName(dateTaken); 1741 ExifInterface exif = Exif.getExif(data); 1742 int orientation = Exif.getOrientation(exif); 1743 1744 mActivity.getMediaSaveService().addImage( 1745 data, title, dateTaken, loc, orientation, 1746 exif, mOnPhotoSavedListener, mContentResolver); 1747 } 1748 1749 private String convertOutputFormatToMimeType(int outputFileFormat) { 1750 if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { 1751 return "video/mp4"; 1752 } 1753 return "video/3gpp"; 1754 } 1755 1756 private String convertOutputFormatToFileExt(int outputFileFormat) { 1757 if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { 1758 return ".mp4"; 1759 } 1760 return ".3gp"; 1761 } 1762 1763 private void closeVideoFileDescriptor() { 1764 if (mVideoFileDescriptor != null) { 1765 try { 1766 mVideoFileDescriptor.close(); 1767 } catch (IOException e) { 1768 Log.e(TAG, "Fail to close fd", e); 1769 } 1770 mVideoFileDescriptor = null; 1771 } 1772 } 1773 1774 private void showTapToSnapshotToast() { 1775 new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0) 1776 .show(); 1777 // Clear the preference. 1778 Editor editor = mPreferences.edit(); 1779 editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false); 1780 editor.apply(); 1781 } 1782 1783 @Override 1784 public boolean updateStorageHintOnResume() { 1785 return true; 1786 } 1787 1788 // required by OnPreferenceChangedListener 1789 @Override 1790 public void onCameraPickerClicked(int cameraId) { 1791 if (mPaused || mPendingSwitchCameraId != -1) return; 1792 1793 mPendingSwitchCameraId = cameraId; 1794 Log.d(TAG, "Start to copy texture."); 1795 // We need to keep a preview frame for the animation before 1796 // releasing the camera. This will trigger onPreviewTextureCopied. 1797 // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); 1798 // Disable all camera controls. 1799 mSwitchingCamera = true; 1800 switchCamera(); 1801 1802 } 1803 1804 @Override 1805 public void onShowSwitcherPopup() { 1806 mUI.onShowSwitcherPopup(); 1807 } 1808 1809 @Override 1810 public void onMediaSaveServiceConnected(MediaSaveService s) { 1811 // do nothing. 1812 } 1813 1814 @Override 1815 public void onPreviewUIReady() { 1816 startPreview(); 1817 } 1818 1819 @Override 1820 public void onPreviewUIDestroyed() { 1821 stopPreview(); 1822 } 1823 } 1824