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.graphics.SurfaceTexture; 32import android.hardware.Camera.CameraInfo; 33import android.hardware.Camera.Parameters; 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.MediaColumns; 48import android.provider.MediaStore.Video; 49import android.util.Log; 50import android.view.KeyEvent; 51import android.view.OrientationEventListener; 52import android.view.View; 53import android.view.WindowManager; 54import android.widget.Toast; 55 56import com.android.camera.CameraManager.CameraPictureCallback; 57import com.android.camera.CameraManager.CameraProxy; 58import com.android.camera.app.OrientationManager; 59import com.android.camera.exif.ExifInterface; 60import com.android.camera.ui.RotateTextToast; 61import com.android.camera.util.AccessibilityUtils; 62import com.android.camera.util.ApiHelper; 63import com.android.camera.util.CameraUtil; 64import com.android.camera.util.UsageStatistics; 65import com.android.camera2.R; 66 67import java.io.File; 68import java.io.IOException; 69import java.text.SimpleDateFormat; 70import java.util.Date; 71import java.util.Iterator; 72import java.util.List; 73 74public 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 setupMediaRecorderPreviewDisplay(); 969 // Unlock the camera object before passing it to media recorder. 970 mCameraDevice.unlock(); 971 mMediaRecorder.setCamera(mCameraDevice.getCamera()); 972 if (!mCaptureTimeLapse) { 973 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); 974 } 975 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 976 mMediaRecorder.setProfile(mProfile); 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 1026 try { 1027 mMediaRecorder.prepare(); 1028 } catch (IOException e) { 1029 Log.e(TAG, "prepare failed for " + mVideoFilename, e); 1030 releaseMediaRecorder(); 1031 throw new RuntimeException(e); 1032 } 1033 1034 mMediaRecorder.setOnErrorListener(this); 1035 mMediaRecorder.setOnInfoListener(this); 1036 } 1037 1038 private static void setCaptureRate(MediaRecorder recorder, double fps) { 1039 recorder.setCaptureRate(fps); 1040 } 1041 1042 private void setRecordLocation() { 1043 Location loc = mLocationManager.getCurrentLocation(); 1044 if (loc != null) { 1045 mMediaRecorder.setLocation((float) loc.getLatitude(), 1046 (float) loc.getLongitude()); 1047 } 1048 } 1049 1050 private void releaseMediaRecorder() { 1051 Log.v(TAG, "Releasing media recorder."); 1052 if (mMediaRecorder != null) { 1053 cleanupEmptyFile(); 1054 mMediaRecorder.reset(); 1055 mMediaRecorder.release(); 1056 mMediaRecorder = null; 1057 } 1058 mVideoFilename = null; 1059 } 1060 1061 private void generateVideoFilename(int outputFileFormat) { 1062 long dateTaken = System.currentTimeMillis(); 1063 String title = createName(dateTaken); 1064 // Used when emailing. 1065 String filename = title + convertOutputFormatToFileExt(outputFileFormat); 1066 String mime = convertOutputFormatToMimeType(outputFileFormat); 1067 String path = Storage.DIRECTORY + '/' + filename; 1068 String tmpPath = path + ".tmp"; 1069 mCurrentVideoValues = new ContentValues(9); 1070 mCurrentVideoValues.put(Video.Media.TITLE, title); 1071 mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename); 1072 mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken); 1073 mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000); 1074 mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime); 1075 mCurrentVideoValues.put(Video.Media.DATA, path); 1076 mCurrentVideoValues.put(Video.Media.RESOLUTION, 1077 Integer.toString(mProfile.videoFrameWidth) + "x" + 1078 Integer.toString(mProfile.videoFrameHeight)); 1079 Location loc = mLocationManager.getCurrentLocation(); 1080 if (loc != null) { 1081 mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude()); 1082 mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude()); 1083 } 1084 mVideoFilename = tmpPath; 1085 Log.v(TAG, "New video filename: " + mVideoFilename); 1086 } 1087 1088 private void saveVideo() { 1089 if (mVideoFileDescriptor == null) { 1090 long duration = SystemClock.uptimeMillis() - mRecordingStartTime; 1091 if (duration > 0) { 1092 if (mCaptureTimeLapse) { 1093 duration = getTimeLapseVideoLength(duration); 1094 } 1095 } else { 1096 Log.w(TAG, "Video duration <= 0 : " + duration); 1097 } 1098 mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename, 1099 duration, mCurrentVideoValues, 1100 mOnVideoSavedListener, mContentResolver); 1101 } 1102 mCurrentVideoValues = null; 1103 } 1104 1105 private void deleteVideoFile(String fileName) { 1106 Log.v(TAG, "Deleting video " + fileName); 1107 File f = new File(fileName); 1108 if (!f.delete()) { 1109 Log.v(TAG, "Could not delete " + fileName); 1110 } 1111 } 1112 1113 private PreferenceGroup filterPreferenceScreenByIntent( 1114 PreferenceGroup screen) { 1115 Intent intent = mActivity.getIntent(); 1116 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 1117 CameraSettings.removePreferenceFromScreen(screen, 1118 CameraSettings.KEY_VIDEO_QUALITY); 1119 } 1120 1121 if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { 1122 CameraSettings.removePreferenceFromScreen(screen, 1123 CameraSettings.KEY_VIDEO_QUALITY); 1124 } 1125 return screen; 1126 } 1127 1128 // from MediaRecorder.OnErrorListener 1129 @Override 1130 public void onError(MediaRecorder mr, int what, int extra) { 1131 Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra); 1132 if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { 1133 // We may have run out of space on the sdcard. 1134 stopVideoRecording(); 1135 mActivity.updateStorageSpaceAndHint(); 1136 } 1137 } 1138 1139 // from MediaRecorder.OnInfoListener 1140 @Override 1141 public void onInfo(MediaRecorder mr, int what, int extra) { 1142 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { 1143 if (mMediaRecorderRecording) onStopVideoRecording(); 1144 } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 1145 if (mMediaRecorderRecording) onStopVideoRecording(); 1146 1147 // Show the toast. 1148 Toast.makeText(mActivity, R.string.video_reach_size_limit, 1149 Toast.LENGTH_LONG).show(); 1150 } 1151 } 1152 1153 /* 1154 * Make sure we're not recording music playing in the background, ask the 1155 * MediaPlaybackService to pause playback. 1156 */ 1157 private void pauseAudioPlayback() { 1158 // Shamelessly copied from MediaPlaybackService.java, which 1159 // should be public, but isn't. 1160 Intent i = new Intent("com.android.music.musicservicecommand"); 1161 i.putExtra("command", "pause"); 1162 1163 mActivity.sendBroadcast(i); 1164 } 1165 1166 // For testing. 1167 public boolean isRecording() { 1168 return mMediaRecorderRecording; 1169 } 1170 1171 private void startVideoRecording() { 1172 Log.v(TAG, "startVideoRecording"); 1173 mUI.cancelAnimations(); 1174 mUI.setSwipingEnabled(false); 1175 1176 mActivity.updateStorageSpaceAndHint(); 1177 if (mActivity.getStorageSpaceBytes() <= Storage.LOW_STORAGE_THRESHOLD_BYTES) { 1178 Log.v(TAG, "Storage issue, ignore the start request"); 1179 return; 1180 } 1181 1182 //?? 1183 //if (!mCameraDevice.waitDone()) return; 1184 mCurrentVideoUri = null; 1185 1186 initializeRecorder(); 1187 if (mMediaRecorder == null) { 1188 Log.e(TAG, "Fail to initialize media recorder"); 1189 return; 1190 } 1191 1192 pauseAudioPlayback(); 1193 1194 try { 1195 mMediaRecorder.start(); // Recording is now started 1196 } catch (RuntimeException e) { 1197 Log.e(TAG, "Could not start media recorder. ", e); 1198 releaseMediaRecorder(); 1199 // If start fails, frameworks will not lock the camera for us. 1200 mCameraDevice.lock(); 1201 return; 1202 } 1203 1204 // Make sure the video recording has started before announcing 1205 // this in accessibility. 1206 AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), 1207 mActivity.getString(R.string.video_recording_started)); 1208 1209 // The parameters might have been altered by MediaRecorder already. 1210 // We need to force mCameraDevice to refresh before getting it. 1211 mCameraDevice.refreshParameters(); 1212 // The parameters may have been changed by MediaRecorder upon starting 1213 // recording. We need to alter the parameters if we support camcorder 1214 // zoom. To reduce latency when setting the parameters during zoom, we 1215 // update mParameters here once. 1216 mParameters = mCameraDevice.getParameters(); 1217 1218 mUI.enableCameraControls(false); 1219 1220 mMediaRecorderRecording = true; 1221 mOrientationManager.lockOrientation(); 1222 mRecordingStartTime = SystemClock.uptimeMillis(); 1223 mUI.showRecordingUI(true); 1224 1225 updateRecordingTime(); 1226 keepScreenOn(); 1227 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 1228 UsageStatistics.ACTION_CAPTURE_START, "Video"); 1229 } 1230 1231 private Bitmap getVideoThumbnail() { 1232 Bitmap bitmap = null; 1233 if (mVideoFileDescriptor != null) { 1234 bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(), 1235 mDesiredPreviewWidth); 1236 } else if (mCurrentVideoUri != null) { 1237 try { 1238 mVideoFileDescriptor = mContentResolver.openFileDescriptor(mCurrentVideoUri, "r"); 1239 bitmap = Thumbnail.createVideoThumbnailBitmap( 1240 mVideoFileDescriptor.getFileDescriptor(), mDesiredPreviewWidth); 1241 } catch (java.io.FileNotFoundException ex) { 1242 // invalid uri 1243 Log.e(TAG, ex.toString()); 1244 } 1245 } 1246 1247 if (bitmap != null) { 1248 // MetadataRetriever already rotates the thumbnail. We should rotate 1249 // it to match the UI orientation (and mirror if it is front-facing camera). 1250 CameraInfo[] info = CameraHolder.instance().getCameraInfo(); 1251 boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT); 1252 bitmap = CameraUtil.rotateAndMirror(bitmap, 0, mirror); 1253 } 1254 return bitmap; 1255 } 1256 1257 private void showCaptureResult() { 1258 mIsInReviewMode = true; 1259 Bitmap bitmap = getVideoThumbnail(); 1260 if (bitmap != null) { 1261 mUI.showReviewImage(bitmap); 1262 } 1263 mUI.showReviewControls(); 1264 mUI.enableCameraControls(false); 1265 mUI.showTimeLapseUI(false); 1266 } 1267 1268 private boolean stopVideoRecording() { 1269 Log.v(TAG, "stopVideoRecording"); 1270 mUI.setSwipingEnabled(true); 1271 if (!isVideoCaptureIntent()) { 1272 mUI.showSwitcher(); 1273 } 1274 1275 boolean fail = false; 1276 if (mMediaRecorderRecording) { 1277 boolean shouldAddToMediaStoreNow = false; 1278 1279 try { 1280 mMediaRecorder.setOnErrorListener(null); 1281 mMediaRecorder.setOnInfoListener(null); 1282 mMediaRecorder.stop(); 1283 shouldAddToMediaStoreNow = true; 1284 mCurrentVideoFilename = mVideoFilename; 1285 Log.v(TAG, "stopVideoRecording: Setting current video filename: " 1286 + mCurrentVideoFilename); 1287 AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), 1288 mActivity.getString(R.string.video_recording_stopped)); 1289 } catch (RuntimeException e) { 1290 Log.e(TAG, "stop fail", e); 1291 if (mVideoFilename != null) deleteVideoFile(mVideoFilename); 1292 fail = true; 1293 } 1294 mMediaRecorderRecording = false; 1295 mOrientationManager.unlockOrientation(); 1296 1297 // If the activity is paused, this means activity is interrupted 1298 // during recording. Release the camera as soon as possible because 1299 // face unlock or other applications may need to use the camera. 1300 if (mPaused) { 1301 closeCamera(); 1302 } 1303 1304 mUI.showRecordingUI(false); 1305 if (!mIsVideoCaptureIntent) { 1306 mUI.enableCameraControls(true); 1307 } 1308 // The orientation was fixed during video recording. Now make it 1309 // reflect the device orientation as video recording is stopped. 1310 mUI.setOrientationIndicator(0, true); 1311 keepScreenOnAwhile(); 1312 if (shouldAddToMediaStoreNow && !fail) { 1313 if (mVideoFileDescriptor == null) { 1314 saveVideo(); 1315 } else if (mIsVideoCaptureIntent) { 1316 // if no file save is needed, we can show the post capture UI now 1317 showCaptureResult(); 1318 } 1319 } 1320 } 1321 // release media recorder 1322 releaseMediaRecorder(); 1323 if (!mPaused) { 1324 mCameraDevice.lock(); 1325 if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { 1326 stopPreview(); 1327 mUI.hideSurfaceView(); 1328 // Switch back to use SurfaceTexture for preview. 1329 startPreview(); 1330 } 1331 } 1332 // Update the parameters here because the parameters might have been altered 1333 // by MediaRecorder. 1334 if (!mPaused) mParameters = mCameraDevice.getParameters(); 1335 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 1336 fail ? UsageStatistics.ACTION_CAPTURE_FAIL : 1337 UsageStatistics.ACTION_CAPTURE_DONE, "Video", 1338 SystemClock.uptimeMillis() - mRecordingStartTime); 1339 return fail; 1340 } 1341 1342 private void resetScreenOn() { 1343 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1344 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1345 } 1346 1347 private void keepScreenOnAwhile() { 1348 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1349 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1350 mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1351 } 1352 1353 private void keepScreenOn() { 1354 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1355 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1356 } 1357 1358 private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) { 1359 long seconds = milliSeconds / 1000; // round down to compute seconds 1360 long minutes = seconds / 60; 1361 long hours = minutes / 60; 1362 long remainderMinutes = minutes - (hours * 60); 1363 long remainderSeconds = seconds - (minutes * 60); 1364 1365 StringBuilder timeStringBuilder = new StringBuilder(); 1366 1367 // Hours 1368 if (hours > 0) { 1369 if (hours < 10) { 1370 timeStringBuilder.append('0'); 1371 } 1372 timeStringBuilder.append(hours); 1373 1374 timeStringBuilder.append(':'); 1375 } 1376 1377 // Minutes 1378 if (remainderMinutes < 10) { 1379 timeStringBuilder.append('0'); 1380 } 1381 timeStringBuilder.append(remainderMinutes); 1382 timeStringBuilder.append(':'); 1383 1384 // Seconds 1385 if (remainderSeconds < 10) { 1386 timeStringBuilder.append('0'); 1387 } 1388 timeStringBuilder.append(remainderSeconds); 1389 1390 // Centi seconds 1391 if (displayCentiSeconds) { 1392 timeStringBuilder.append('.'); 1393 long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10; 1394 if (remainderCentiSeconds < 10) { 1395 timeStringBuilder.append('0'); 1396 } 1397 timeStringBuilder.append(remainderCentiSeconds); 1398 } 1399 1400 return timeStringBuilder.toString(); 1401 } 1402 1403 private long getTimeLapseVideoLength(long deltaMs) { 1404 // For better approximation calculate fractional number of frames captured. 1405 // This will update the video time at a higher resolution. 1406 double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs; 1407 return (long) (numberOfFrames / mProfile.videoFrameRate * 1000); 1408 } 1409 1410 private void updateRecordingTime() { 1411 if (!mMediaRecorderRecording) { 1412 return; 1413 } 1414 long now = SystemClock.uptimeMillis(); 1415 long delta = now - mRecordingStartTime; 1416 1417 // Starting a minute before reaching the max duration 1418 // limit, we'll countdown the remaining time instead. 1419 boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0 1420 && delta >= mMaxVideoDurationInMs - 60000); 1421 1422 long deltaAdjusted = delta; 1423 if (countdownRemainingTime) { 1424 deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999; 1425 } 1426 String text; 1427 1428 long targetNextUpdateDelay; 1429 if (!mCaptureTimeLapse) { 1430 text = millisecondToTimeString(deltaAdjusted, false); 1431 targetNextUpdateDelay = 1000; 1432 } else { 1433 // The length of time lapse video is different from the length 1434 // of the actual wall clock time elapsed. Display the video length 1435 // only in format hh:mm:ss.dd, where dd are the centi seconds. 1436 text = millisecondToTimeString(getTimeLapseVideoLength(delta), true); 1437 targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs; 1438 } 1439 1440 mUI.setRecordingTime(text); 1441 1442 if (mRecordingTimeCountsDown != countdownRemainingTime) { 1443 // Avoid setting the color on every update, do it only 1444 // when it needs changing. 1445 mRecordingTimeCountsDown = countdownRemainingTime; 1446 1447 int color = mActivity.getResources().getColor(countdownRemainingTime 1448 ? R.color.recording_time_remaining_text 1449 : R.color.recording_time_elapsed_text); 1450 1451 mUI.setRecordingTimeTextColor(color); 1452 } 1453 1454 long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay); 1455 mHandler.sendEmptyMessageDelayed( 1456 UPDATE_RECORD_TIME, actualNextUpdateDelay); 1457 } 1458 1459 private static boolean isSupported(String value, List<String> supported) { 1460 return supported == null ? false : supported.indexOf(value) >= 0; 1461 } 1462 1463 @SuppressWarnings("deprecation") 1464 private void setCameraParameters() { 1465 mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); 1466 int[] fpsRange = CameraUtil.getMaxPreviewFpsRange(mParameters); 1467 if (fpsRange.length > 0) { 1468 mParameters.setPreviewFpsRange( 1469 fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], 1470 fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); 1471 } else { 1472 mParameters.setPreviewFrameRate(mProfile.videoFrameRate); 1473 } 1474 1475 forceFlashOffIfSupported(!mUI.isVisible()); 1476 1477 // Set white balance parameter. 1478 String whiteBalance = mPreferences.getString( 1479 CameraSettings.KEY_WHITE_BALANCE, 1480 mActivity.getString(R.string.pref_camera_whitebalance_default)); 1481 if (isSupported(whiteBalance, 1482 mParameters.getSupportedWhiteBalance())) { 1483 mParameters.setWhiteBalance(whiteBalance); 1484 } else { 1485 whiteBalance = mParameters.getWhiteBalance(); 1486 if (whiteBalance == null) { 1487 whiteBalance = Parameters.WHITE_BALANCE_AUTO; 1488 } 1489 } 1490 1491 // Set zoom. 1492 if (mParameters.isZoomSupported()) { 1493 mParameters.setZoom(mZoomValue); 1494 } 1495 1496 // Set continuous autofocus. 1497 List<String> supportedFocus = mParameters.getSupportedFocusModes(); 1498 if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) { 1499 mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); 1500 } 1501 1502 mParameters.set(CameraUtil.RECORDING_HINT, CameraUtil.TRUE); 1503 1504 // Enable video stabilization. Convenience methods not available in API 1505 // level <= 14 1506 String vstabSupported = mParameters.get("video-stabilization-supported"); 1507 if ("true".equals(vstabSupported)) { 1508 mParameters.set("video-stabilization", "true"); 1509 } 1510 1511 // Set picture size. 1512 // The logic here is different from the logic in still-mode camera. 1513 // There we determine the preview size based on the picture size, but 1514 // here we determine the picture size based on the preview size. 1515 List<Size> supported = mParameters.getSupportedPictureSizes(); 1516 Size optimalSize = CameraUtil.getOptimalVideoSnapshotPictureSize(supported, 1517 (double) mDesiredPreviewWidth / mDesiredPreviewHeight); 1518 Size original = mParameters.getPictureSize(); 1519 if (!original.equals(optimalSize)) { 1520 mParameters.setPictureSize(optimalSize.width, optimalSize.height); 1521 } 1522 Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" + 1523 optimalSize.height); 1524 1525 // Set JPEG quality. 1526 int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, 1527 CameraProfile.QUALITY_HIGH); 1528 mParameters.setJpegQuality(jpegQuality); 1529 1530 mCameraDevice.setParameters(mParameters); 1531 // Keep preview size up to date. 1532 mParameters = mCameraDevice.getParameters(); 1533 1534 // Update UI based on the new parameters. 1535 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1536 } 1537 1538 @Override 1539 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1540 // Do nothing. 1541 } 1542 1543 @Override 1544 public void onConfigurationChanged(Configuration newConfig) { 1545 Log.v(TAG, "onConfigurationChanged"); 1546 setDisplayOrientation(); 1547 } 1548 1549 @Override 1550 public void onOverriddenPreferencesClicked() { 1551 } 1552 1553 @Override 1554 // TODO: Delete this after old camera code is removed 1555 public void onRestorePreferencesClicked() { 1556 } 1557 1558 @Override 1559 public void onSharedPreferenceChanged() { 1560 // ignore the events after "onPause()" or preview has not started yet 1561 if (mPaused) { 1562 return; 1563 } 1564 synchronized (mPreferences) { 1565 // If mCameraDevice is not ready then we can set the parameter in 1566 // startPreview(). 1567 if (mCameraDevice == null) return; 1568 1569 boolean recordLocation = RecordLocationPreference.get( 1570 mPreferences, mContentResolver); 1571 mLocationManager.recordLocation(recordLocation); 1572 1573 readVideoPreferences(); 1574 mUI.showTimeLapseUI(mCaptureTimeLapse); 1575 // We need to restart the preview if preview size is changed. 1576 Size size = mParameters.getPreviewSize(); 1577 if (size.width != mDesiredPreviewWidth 1578 || size.height != mDesiredPreviewHeight) { 1579 1580 stopPreview(); 1581 resizeForPreviewAspectRatio(); 1582 startPreview(); // Parameters will be set in startPreview(). 1583 } else { 1584 setCameraParameters(); 1585 } 1586 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1587 } 1588 } 1589 1590 protected void setCameraId(int cameraId) { 1591 ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); 1592 pref.setValue("" + cameraId); 1593 } 1594 1595 private void switchCamera() { 1596 if (mPaused) { 1597 return; 1598 } 1599 1600 Log.d(TAG, "Start to switch camera."); 1601 mCameraId = mPendingSwitchCameraId; 1602 mPendingSwitchCameraId = -1; 1603 setCameraId(mCameraId); 1604 1605 closeCamera(); 1606 mUI.collapseCameraControls(); 1607 // Restart the camera and initialize the UI. From onCreate. 1608 mPreferences.setLocalId(mActivity, mCameraId); 1609 CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); 1610 openCamera(); 1611 readVideoPreferences(); 1612 startPreview(); 1613 initializeVideoSnapshot(); 1614 resizeForPreviewAspectRatio(); 1615 initializeVideoControl(); 1616 1617 // From onResume 1618 mZoomValue = 0; 1619 mUI.initializeZoom(mParameters); 1620 mUI.setOrientationIndicator(0, false); 1621 1622 // Start switch camera animation. Post a message because 1623 // onFrameAvailable from the old camera may already exist. 1624 mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); 1625 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1626 } 1627 1628 // Preview texture has been copied. Now camera can be released and the 1629 // animation can be started. 1630 @Override 1631 public void onPreviewTextureCopied() { 1632 mHandler.sendEmptyMessage(SWITCH_CAMERA); 1633 } 1634 1635 @Override 1636 public void onCaptureTextureCopied() { 1637 } 1638 1639 private void initializeVideoSnapshot() { 1640 if (mParameters == null) return; 1641 if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { 1642 // Show the tap to focus toast if this is the first start. 1643 if (mPreferences.getBoolean( 1644 CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) { 1645 // Delay the toast for one second to wait for orientation. 1646 mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000); 1647 } 1648 } 1649 } 1650 1651 void showVideoSnapshotUI(boolean enabled) { 1652 if (mParameters == null) return; 1653 if (CameraUtil.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { 1654 if (enabled) { 1655 mUI.animateFlash(); 1656 mUI.animateCapture(); 1657 } else { 1658 mUI.showPreviewBorder(enabled); 1659 } 1660 mUI.enableShutter(!enabled); 1661 } 1662 } 1663 1664 private void forceFlashOffIfSupported(boolean forceOff) { 1665 String flashMode; 1666 if (!forceOff) { 1667 flashMode = mPreferences.getString( 1668 CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, 1669 mActivity.getString(R.string.pref_camera_video_flashmode_default)); 1670 } else { 1671 flashMode = Parameters.FLASH_MODE_OFF; 1672 } 1673 List<String> supportedFlash = mParameters.getSupportedFlashModes(); 1674 if (isSupported(flashMode, supportedFlash)) { 1675 mParameters.setFlashMode(flashMode); 1676 } else { 1677 flashMode = mParameters.getFlashMode(); 1678 if (flashMode == null) { 1679 flashMode = mActivity.getString( 1680 R.string.pref_camera_flashmode_no_flash); 1681 } 1682 } 1683 } 1684 1685 /** 1686 * Used to update the flash mode. Video mode can turn on the flash as torch 1687 * mode, which we would like to turn on and off when we switching in and 1688 * out to the preview. 1689 * 1690 * @param forceOff whether we want to force the flash off. 1691 */ 1692 private void forceFlashOff(boolean forceOff) { 1693 if (!mPreviewing || mParameters.getFlashMode() == null) { 1694 return; 1695 } 1696 forceFlashOffIfSupported(forceOff); 1697 mCameraDevice.setParameters(mParameters); 1698 mUI.updateOnScreenIndicators(mParameters, mPreferences); 1699 } 1700 1701 @Override 1702 public void onPreviewFocusChanged(boolean previewFocused) { 1703 mUI.onPreviewFocusChanged(previewFocused); 1704 forceFlashOff(!previewFocused); 1705 } 1706 1707 @Override 1708 public boolean arePreviewControlsVisible() { 1709 return mUI.arePreviewControlsVisible(); 1710 } 1711 1712 private final class JpegPictureCallback implements CameraPictureCallback { 1713 Location mLocation; 1714 1715 public JpegPictureCallback(Location loc) { 1716 mLocation = loc; 1717 } 1718 1719 @Override 1720 public void onPictureTaken(byte [] jpegData, CameraProxy camera) { 1721 Log.v(TAG, "onPictureTaken"); 1722 mSnapshotInProgress = false; 1723 showVideoSnapshotUI(false); 1724 storeImage(jpegData, mLocation); 1725 } 1726 } 1727 1728 private void storeImage(final byte[] data, Location loc) { 1729 long dateTaken = System.currentTimeMillis(); 1730 String title = CameraUtil.createJpegName(dateTaken); 1731 ExifInterface exif = Exif.getExif(data); 1732 int orientation = Exif.getOrientation(exif); 1733 1734 mActivity.getMediaSaveService().addImage( 1735 data, title, dateTaken, loc, orientation, 1736 exif, mOnPhotoSavedListener, mContentResolver); 1737 } 1738 1739 private String convertOutputFormatToMimeType(int outputFileFormat) { 1740 if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { 1741 return "video/mp4"; 1742 } 1743 return "video/3gpp"; 1744 } 1745 1746 private String convertOutputFormatToFileExt(int outputFileFormat) { 1747 if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { 1748 return ".mp4"; 1749 } 1750 return ".3gp"; 1751 } 1752 1753 private void closeVideoFileDescriptor() { 1754 if (mVideoFileDescriptor != null) { 1755 try { 1756 mVideoFileDescriptor.close(); 1757 } catch (IOException e) { 1758 Log.e(TAG, "Fail to close fd", e); 1759 } 1760 mVideoFileDescriptor = null; 1761 } 1762 } 1763 1764 private void showTapToSnapshotToast() { 1765 new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0) 1766 .show(); 1767 // Clear the preference. 1768 Editor editor = mPreferences.edit(); 1769 editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false); 1770 editor.apply(); 1771 } 1772 1773 @Override 1774 public boolean updateStorageHintOnResume() { 1775 return true; 1776 } 1777 1778 // required by OnPreferenceChangedListener 1779 @Override 1780 public void onCameraPickerClicked(int cameraId) { 1781 if (mPaused || mPendingSwitchCameraId != -1) return; 1782 1783 mPendingSwitchCameraId = cameraId; 1784 Log.d(TAG, "Start to copy texture."); 1785 // We need to keep a preview frame for the animation before 1786 // releasing the camera. This will trigger onPreviewTextureCopied. 1787 // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); 1788 // Disable all camera controls. 1789 mSwitchingCamera = true; 1790 switchCamera(); 1791 1792 } 1793 1794 @Override 1795 public void onShowSwitcherPopup() { 1796 mUI.onShowSwitcherPopup(); 1797 } 1798 1799 @Override 1800 public void onMediaSaveServiceConnected(MediaSaveService s) { 1801 // do nothing. 1802 } 1803 1804 @Override 1805 public void onPreviewUIReady() { 1806 startPreview(); 1807 } 1808 1809 @Override 1810 public void onPreviewUIDestroyed() { 1811 stopPreview(); 1812 } 1813} 1814