VideoCamera.java revision d87cb2b7fec4fa2d4ccfb8600a5eb60bae9facf7
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.camera; 18 19import com.android.camera.gallery.IImage; 20import com.android.camera.gallery.IImageList; 21import com.android.camera.ui.CamcorderHeadUpDisplay; 22import com.android.camera.ui.GLRootView; 23import com.android.camera.ui.GLView; 24import com.android.camera.ui.HeadUpDisplay; 25 26import android.content.ActivityNotFoundException; 27import android.content.BroadcastReceiver; 28import android.content.ContentResolver; 29import android.content.ContentValues; 30import android.content.Context; 31import android.content.Intent; 32import android.content.IntentFilter; 33import android.content.SharedPreferences; 34import android.content.res.Configuration; 35import android.content.res.Resources; 36import android.graphics.Bitmap; 37import android.graphics.drawable.Drawable; 38import android.hardware.Camera.Parameters; 39import android.hardware.Camera.Size; 40import android.media.CamcorderProfile; 41import android.media.MediaRecorder; 42import android.media.ThumbnailUtils; 43import android.net.Uri; 44import android.os.Build; 45import android.os.Bundle; 46import android.os.Environment; 47import android.os.Handler; 48import android.os.ParcelFileDescriptor; 49import android.os.Message; 50import android.os.StatFs; 51import android.os.SystemClock; 52import android.provider.MediaStore; 53import android.provider.Settings; 54import android.provider.MediaStore.Video; 55import android.util.Log; 56import android.view.Display; 57import android.view.KeyEvent; 58import android.view.LayoutInflater; 59import android.view.Menu; 60import android.view.MenuItem; 61import android.view.SurfaceHolder; 62import android.view.SurfaceView; 63import android.view.View; 64import android.view.ViewGroup; 65import android.view.Window; 66import android.view.WindowManager; 67import android.view.MenuItem.OnMenuItemClickListener; 68import android.view.animation.AlphaAnimation; 69import android.view.animation.Animation; 70import android.widget.FrameLayout; 71import android.widget.ImageView; 72import android.widget.TextView; 73import android.widget.Toast; 74 75import java.io.File; 76import java.io.IOException; 77import java.text.SimpleDateFormat; 78import java.util.ArrayList; 79import java.util.Date; 80import java.util.List; 81 82/** 83 * The Camcorder activity. 84 */ 85public class VideoCamera extends NoSearchActivity 86 implements View.OnClickListener, 87 ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback, 88 MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener, 89 Switcher.OnSwitchListener, PreviewFrameLayout.OnSizeChangedListener { 90 91 private static final String TAG = "videocamera"; 92 93 private static final int INIT_RECORDER = 3; 94 private static final int CLEAR_SCREEN_DELAY = 4; 95 private static final int UPDATE_RECORD_TIME = 5; 96 private static final int ENABLE_SHUTTER_BUTTON = 6; 97 98 private static final int SCREEN_DELAY = 2 * 60 * 1000; 99 100 // The brightness settings used when it is set to automatic in the system. 101 // The reason why it is set to 0.7 is just because 1.0 is too bright. 102 private static final float DEFAULT_CAMERA_BRIGHTNESS = 0.7f; 103 104 private static final long NO_STORAGE_ERROR = -1L; 105 private static final long CANNOT_STAT_ERROR = -2L; 106 private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L; 107 108 private static final int STORAGE_STATUS_OK = 0; 109 private static final int STORAGE_STATUS_LOW = 1; 110 private static final int STORAGE_STATUS_NONE = 2; 111 private static final int STORAGE_STATUS_FAIL = 3; 112 113 private static final boolean SWITCH_CAMERA = true; 114 private static final boolean SWITCH_VIDEO = false; 115 116 private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms 117 118 /** 119 * An unpublished intent flag requesting to start recording straight away 120 * and return as soon as recording is stopped. 121 * TODO: consider publishing by moving into MediaStore. 122 */ 123 private final static String EXTRA_QUICK_CAPTURE = 124 "android.intent.extra.quickCapture"; 125 126 private ComboPreferences mPreferences; 127 128 private PreviewFrameLayout mPreviewFrameLayout; 129 private SurfaceView mVideoPreview; 130 private SurfaceHolder mSurfaceHolder = null; 131 private ImageView mVideoFrame; 132 private GLRootView mGLRootView; 133 private CamcorderHeadUpDisplay mHeadUpDisplay; 134 private MenuItem mSwitchTimeLapseMenuItem; 135 136 private boolean mIsVideoCaptureIntent; 137 private boolean mQuickCapture; 138 // mLastPictureButton and mThumbController 139 // are non-null only if mIsVideoCaptureIntent is true. 140 private ImageView mLastPictureButton; 141 private ThumbnailController mThumbController; 142 private boolean mStartPreviewFail = false; 143 144 private int mStorageStatus = STORAGE_STATUS_OK; 145 146 private MediaRecorder mMediaRecorder; 147 private boolean mMediaRecorderRecording = false; 148 private long mRecordingStartTime; 149 // The video file that the hardware camera is about to record into 150 // (or is recording into.) 151 private String mVideoFilename; 152 private ParcelFileDescriptor mVideoFileDescriptor; 153 154 // The video file that has already been recorded, and that is being 155 // examined by the user. 156 private String mCurrentVideoFilename; 157 private Uri mCurrentVideoUri; 158 private ContentValues mCurrentVideoValues; 159 160 private CamcorderProfile mProfile; 161 162 // The video duration limit. 0 menas no limit. 163 private int mMaxVideoDurationInMs; 164 165 // Time Lapse parameters. 166 private boolean mCaptureTimeLapse = false; 167 private boolean mUseStillCameraForTimeLapse = false; 168 private int mTimeBetweenTimeLapseFrameCaptureMs = 2000; 169 private int mEncoderLevel; 170 171 private int mDesiredPreviewWidth; 172 private int mDesiredPreviewHeight; 173 174 boolean mPausing = false; 175 boolean mSwitching; 176 boolean mPreviewing = false; // True if preview is started. 177 178 private ContentResolver mContentResolver; 179 180 private ShutterButton mShutterButton; 181 private TextView mRecordingTimeView; 182 private Switcher mSwitcher; 183 private boolean mRecordingTimeCountsDown = false; 184 185 private final ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>(); 186 187 private final Handler mHandler = new MainHandler(); 188 private Parameters mParameters; 189 190 // multiple cameras support 191 private int mNumberOfCameras; 192 private int mCameraId; 193 194 // This Handler is used to post message back onto the main thread of the 195 // application 196 private class MainHandler extends Handler { 197 @Override 198 public void handleMessage(Message msg) { 199 switch (msg.what) { 200 201 case ENABLE_SHUTTER_BUTTON: 202 mShutterButton.setEnabled(true); 203 break; 204 205 case CLEAR_SCREEN_DELAY: { 206 getWindow().clearFlags( 207 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 208 break; 209 } 210 211 case UPDATE_RECORD_TIME: { 212 updateRecordingTime(); 213 break; 214 } 215 216 case INIT_RECORDER: { 217 initializeRecorder(); 218 break; 219 } 220 221 default: 222 Log.v(TAG, "Unhandled message: " + msg.what); 223 break; 224 } 225 } 226 } 227 228 private BroadcastReceiver mReceiver = null; 229 230 private class MyBroadcastReceiver extends BroadcastReceiver { 231 @Override 232 public void onReceive(Context context, Intent intent) { 233 String action = intent.getAction(); 234 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 235 updateAndShowStorageHint(false); 236 stopVideoRecording(); 237 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 238 updateAndShowStorageHint(true); 239 initializeRecorder(); 240 } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 241 // SD card unavailable 242 // handled in ACTION_MEDIA_EJECT 243 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { 244 Toast.makeText(VideoCamera.this, 245 getResources().getString(R.string.wait), 5000); 246 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { 247 updateAndShowStorageHint(true); 248 } 249 } 250 } 251 252 private String createName(long dateTaken) { 253 Date date = new Date(dateTaken); 254 SimpleDateFormat dateFormat = new SimpleDateFormat( 255 getString(R.string.video_file_name_format)); 256 257 return dateFormat.format(date); 258 } 259 260 private void showCameraErrorAndFinish() { 261 Resources ress = getResources(); 262 Util.showFatalErrorAndFinish(VideoCamera.this, 263 ress.getString(R.string.camera_error_title), 264 ress.getString(R.string.cannot_connect_camera)); 265 } 266 267 private boolean restartPreview() { 268 try { 269 startPreview(); 270 } catch (CameraHardwareException e) { 271 showCameraErrorAndFinish(); 272 return false; 273 } 274 return true; 275 } 276 277 @Override 278 public void onCreate(Bundle icicle) { 279 super.onCreate(icicle); 280 281 Window win = getWindow(); 282 283 // Overright the brightness settings if it is automatic 284 int mode = Settings.System.getInt( 285 getContentResolver(), 286 Settings.System.SCREEN_BRIGHTNESS_MODE, 287 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); 288 if (mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) { 289 WindowManager.LayoutParams winParams = win.getAttributes(); 290 winParams.screenBrightness = DEFAULT_CAMERA_BRIGHTNESS; 291 win.setAttributes(winParams); 292 } 293 294 mPreferences = new ComboPreferences(this); 295 CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); 296 mCameraId = CameraSettings.readPreferredCameraId(mPreferences); 297 mPreferences.setLocalId(this, mCameraId); 298 CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); 299 300 mNumberOfCameras = CameraHolder.instance().getNumberOfCameras(); 301 302 readVideoPreferences(); 303 304 /* 305 * To reduce startup time, we start the preview in another thread. 306 * We make sure the preview is started at the end of onCreate. 307 */ 308 Thread startPreviewThread = new Thread(new Runnable() { 309 public void run() { 310 try { 311 mStartPreviewFail = false; 312 startPreview(); 313 } catch (CameraHardwareException e) { 314 // In eng build, we throw the exception so that test tool 315 // can detect it and report it 316 if ("eng".equals(Build.TYPE)) { 317 throw new RuntimeException(e); 318 } 319 mStartPreviewFail = true; 320 } 321 } 322 }); 323 startPreviewThread.start(); 324 325 mContentResolver = getContentResolver(); 326 327 requestWindowFeature(Window.FEATURE_PROGRESS); 328 setContentView(R.layout.video_camera); 329 330 mPreviewFrameLayout = (PreviewFrameLayout) 331 findViewById(R.id.frame_layout); 332 mPreviewFrameLayout.setOnSizeChangedListener(this); 333 resizeForPreviewAspectRatio(); 334 335 mVideoPreview = (SurfaceView) findViewById(R.id.camera_preview); 336 mVideoFrame = (ImageView) findViewById(R.id.video_frame); 337 338 // don't set mSurfaceHolder here. We have it set ONLY within 339 // surfaceCreated / surfaceDestroyed, other parts of the code 340 // assume that when it is set, the surface is also set. 341 SurfaceHolder holder = mVideoPreview.getHolder(); 342 holder.addCallback(this); 343 holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 344 345 mIsVideoCaptureIntent = isVideoCaptureIntent(); 346 mQuickCapture = getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); 347 mRecordingTimeView = (TextView) findViewById(R.id.recording_time); 348 349 ViewGroup rootView = (ViewGroup) findViewById(R.id.video_camera); 350 LayoutInflater inflater = this.getLayoutInflater(); 351 if (!mIsVideoCaptureIntent) { 352 View controlBar = inflater.inflate( 353 R.layout.camera_control, rootView); 354 mLastPictureButton = 355 (ImageView) controlBar.findViewById(R.id.review_thumbnail); 356 mThumbController = new ThumbnailController( 357 getResources(), mLastPictureButton, mContentResolver); 358 mLastPictureButton.setOnClickListener(this); 359 mThumbController.loadData(ImageManager.getLastVideoThumbPath()); 360 mSwitcher = ((Switcher) findViewById(R.id.camera_switch)); 361 mSwitcher.setOnSwitchListener(this); 362 mSwitcher.addTouchView(findViewById(R.id.camera_switch_set)); 363 } else { 364 View controlBar = inflater.inflate( 365 R.layout.attach_camera_control, rootView); 366 controlBar.findViewById(R.id.btn_cancel).setOnClickListener(this); 367 ImageView retake = 368 (ImageView) controlBar.findViewById(R.id.btn_retake); 369 retake.setOnClickListener(this); 370 retake.setImageResource(R.drawable.btn_ic_review_retake_video); 371 controlBar.findViewById(R.id.btn_play).setOnClickListener(this); 372 controlBar.findViewById(R.id.btn_done).setOnClickListener(this); 373 } 374 375 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 376 mShutterButton.setImageResource(R.drawable.btn_ic_video_record); 377 mShutterButton.setOnShutterButtonListener(this); 378 mShutterButton.requestFocus(); 379 380 // Make sure preview is started. 381 try { 382 startPreviewThread.join(); 383 if (mStartPreviewFail) { 384 showCameraErrorAndFinish(); 385 return; 386 } 387 } catch (InterruptedException ex) { 388 // ignore 389 } 390 391 // Initialize the HeadUpDiplay after startPreview(). We need mParameters 392 // for HeadUpDisplay and it is initialized in that function. 393 initializeHeadUpDisplay(); 394 } 395 396 private void changeHeadUpDisplayState() { 397 // If the camera resumes behind the lock screen, the orientation 398 // will be portrait. That causes OOM when we try to allocation GPU 399 // memory for the GLSurfaceView again when the orientation changes. So, 400 // we delayed initialization of HeadUpDisplay until the orientation 401 // becomes landscape. 402 Configuration config = getResources().getConfiguration(); 403 if (config.orientation == Configuration.ORIENTATION_LANDSCAPE 404 && !mPausing && !mSwitching && mGLRootView == null) { 405 attachHeadUpDisplay(); 406 } else if (mGLRootView != null) { 407 detachHeadUpDisplay(); 408 } 409 } 410 411 private void initializeHeadUpDisplay() { 412 mHeadUpDisplay = new CamcorderHeadUpDisplay(this, mCaptureTimeLapse); 413 CameraSettings settings = new CameraSettings(this, mParameters); 414 415 PreferenceGroup group = settings.getPreferenceGroup(R.xml.video_preferences); 416 if (mIsVideoCaptureIntent) { 417 group = filterPreferenceScreenByIntent(group); 418 } 419 mHeadUpDisplay.initialize(this, group); 420 mHeadUpDisplay.setListener(new MyHeadUpDisplayListener()); 421 } 422 423 private void attachHeadUpDisplay() { 424 FrameLayout frame = (FrameLayout) findViewById(R.id.frame); 425 mGLRootView = new GLRootView(this); 426 frame.addView(mGLRootView); 427 mGLRootView.setContentPane(mHeadUpDisplay); 428 } 429 430 private void detachHeadUpDisplay() { 431 mHeadUpDisplay.collapse(); 432 ((ViewGroup) mGLRootView.getParent()).removeView(mGLRootView); 433 mGLRootView = null; 434 } 435 436 @Override 437 protected void onStart() { 438 super.onStart(); 439 if (!mIsVideoCaptureIntent) { 440 mSwitcher.setSwitch(SWITCH_VIDEO); 441 } 442 } 443 444 private void startPlayVideoActivity() { 445 Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri); 446 try { 447 startActivity(intent); 448 } catch (android.content.ActivityNotFoundException ex) { 449 Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); 450 } 451 } 452 453 public void onClick(View v) { 454 switch (v.getId()) { 455 case R.id.btn_retake: 456 discardCurrentVideoAndInitRecorder(); 457 break; 458 case R.id.btn_play: 459 startPlayVideoActivity(); 460 break; 461 case R.id.btn_done: 462 doReturnToCaller(true); 463 break; 464 case R.id.btn_cancel: 465 stopVideoRecordingAndReturn(false); 466 break; 467 case R.id.review_thumbnail: 468 if (!mMediaRecorderRecording) viewLastVideo(); 469 break; 470 } 471 } 472 473 public void onShutterButtonFocus(ShutterButton button, boolean pressed) { 474 // Do nothing (everything happens in onShutterButtonClick). 475 } 476 477 private void onStopVideoRecording(boolean valid) { 478 if (mIsVideoCaptureIntent) { 479 if (mQuickCapture) { 480 stopVideoRecordingAndReturn(valid); 481 } else { 482 stopVideoRecordingAndShowAlert(); 483 } 484 } else { 485 stopVideoRecordingAndGetThumbnail(); 486 initializeRecorder(); 487 } 488 } 489 490 public void onShutterButtonClick(ShutterButton button) { 491 switch (button.getId()) { 492 case R.id.shutter_button: 493 if (mHeadUpDisplay.collapse()) return; 494 495 if (mMediaRecorderRecording) { 496 onStopVideoRecording(true); 497 } else if (mMediaRecorder != null) { 498 // If the click comes before recorder initialization, it is 499 // ignored. If users click the button during initialization, 500 // the event is put in the queue and record will be started 501 // eventually. 502 startVideoRecording(); 503 } 504 mShutterButton.setEnabled(false); 505 mHandler.sendEmptyMessageDelayed( 506 ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT); 507 break; 508 } 509 } 510 511 private void discardCurrentVideoAndInitRecorder() { 512 deleteCurrentVideo(); 513 hideAlertAndInitializeRecorder(); 514 } 515 516 private OnScreenHint mStorageHint; 517 518 private void updateAndShowStorageHint(boolean mayHaveSd) { 519 mStorageStatus = getStorageStatus(mayHaveSd); 520 showStorageHint(); 521 } 522 523 private void showStorageHint() { 524 String errorMessage = null; 525 switch (mStorageStatus) { 526 case STORAGE_STATUS_NONE: 527 errorMessage = getString(R.string.no_storage); 528 break; 529 case STORAGE_STATUS_LOW: 530 errorMessage = getString(R.string.spaceIsLow_content); 531 break; 532 case STORAGE_STATUS_FAIL: 533 errorMessage = getString(R.string.access_sd_fail); 534 break; 535 } 536 if (errorMessage != null) { 537 if (mStorageHint == null) { 538 mStorageHint = OnScreenHint.makeText(this, errorMessage); 539 } else { 540 mStorageHint.setText(errorMessage); 541 } 542 mStorageHint.show(); 543 } else if (mStorageHint != null) { 544 mStorageHint.cancel(); 545 mStorageHint = null; 546 } 547 } 548 549 private int getStorageStatus(boolean mayHaveSd) { 550 long remaining = mayHaveSd ? getAvailableStorage() : NO_STORAGE_ERROR; 551 if (remaining == NO_STORAGE_ERROR) { 552 return STORAGE_STATUS_NONE; 553 } else if (remaining == CANNOT_STAT_ERROR) { 554 return STORAGE_STATUS_FAIL; 555 } 556 return remaining < LOW_STORAGE_THRESHOLD 557 ? STORAGE_STATUS_LOW 558 : STORAGE_STATUS_OK; 559 } 560 561 private void readTimeLapseVideoPreferences() { 562 String qualityStr = mPreferences.getString( 563 CameraSettings.KEY_VIDEO_TIME_LAPSE_QUALITY, 564 CameraSettings.DEFAULT_VIDEO_TIME_LAPSE_QUALITY_VALUE); 565 566 int quality = CameraSettings.getVideoTimeLapseQuality(qualityStr); 567 568 mProfile = CamcorderProfile.get( 569 (quality == CameraSettings.TIME_LAPSE_VIDEO_QUALITY_LOW) 570 ? CamcorderProfile.QUALITY_LOW 571 : CamcorderProfile.QUALITY_HIGH); 572 573 mMaxVideoDurationInMs = 0; // No limit 574 mTimeBetweenTimeLapseFrameCaptureMs = 2000; 575 576 // TODO: Add new profiles for time lapse instead of setting mProfile 577 // values here. 578 switch (quality) { 579 case CameraSettings.TIME_LAPSE_VIDEO_QUALITY_LOW: 580 mUseStillCameraForTimeLapse = false; 581 mProfile.videoFrameWidth = 176; 582 mProfile.videoFrameHeight = 144; 583 mEncoderLevel = 0; 584 break; 585 586 case CameraSettings.TIME_LAPSE_VIDEO_QUALITY_HIGH: 587 mUseStillCameraForTimeLapse = false; 588 mProfile.videoFrameWidth = 720; 589 mProfile.videoFrameHeight = 480; 590 mEncoderLevel = 0; 591 break; 592 593 case CameraSettings.TIME_LAPSE_VIDEO_QUALITY_720P: 594 mUseStillCameraForTimeLapse = false; 595 mProfile.videoFrameWidth = 1280; 596 mProfile.videoFrameHeight = 720; 597 mProfile.videoBitRate = 20000000; 598 mEncoderLevel = 50; 599 break; 600 601 case CameraSettings.TIME_LAPSE_VIDEO_QUALITY_1080P: 602 mUseStillCameraForTimeLapse = true; 603 mProfile.videoFrameWidth = 1920; 604 mProfile.videoFrameHeight = 1088; 605 mProfile.videoBitRate = 20000000; 606 mEncoderLevel = 50; 607 break; 608 } 609 610 if (mUseStillCameraForTimeLapse) { 611 // When using still camera for capturing time lapse frames 612 // mProfile.{videoFrameWidth,videoFrameHeight} may correspond to 613 // HD resolution not supported by the video camera. So choose 614 // preview size optimally from the supported preview sizes. 615 List<Size> sizes = mParameters.getSupportedPreviewSizes(); 616 Size optimalSize = Util.getOptimalPreviewSize(this, 617 sizes, (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); 618 mDesiredPreviewWidth = optimalSize.width; 619 mDesiredPreviewHeight = optimalSize.height; 620 } else { 621 mDesiredPreviewWidth = mProfile.videoFrameWidth; 622 mDesiredPreviewHeight = mProfile.videoFrameHeight; 623 } 624 } 625 626 private void readVideoPreferences() { 627 if (mCaptureTimeLapse) { 628 readTimeLapseVideoPreferences(); 629 return; 630 } 631 632 String quality = mPreferences.getString( 633 CameraSettings.KEY_VIDEO_QUALITY, 634 CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE); 635 636 boolean videoQualityHigh = CameraSettings.getVideoQuality(quality); 637 638 // Set video quality. 639 Intent intent = getIntent(); 640 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 641 int extraVideoQuality = 642 intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 643 videoQualityHigh = (extraVideoQuality > 0); 644 } 645 646 // Set video duration limit. The limit is read from the preference, 647 // unless it is specified in the intent. 648 if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { 649 int seconds = 650 intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0); 651 mMaxVideoDurationInMs = 1000 * seconds; 652 } else { 653 mMaxVideoDurationInMs = 654 CameraSettings.getVidoeDurationInMillis(quality); 655 } 656 mProfile = CamcorderProfile.get(videoQualityHigh 657 ? CamcorderProfile.QUALITY_HIGH 658 : CamcorderProfile.QUALITY_LOW); 659 660 mDesiredPreviewWidth = mProfile.videoFrameWidth; 661 mDesiredPreviewHeight = mProfile.videoFrameHeight; 662 } 663 664 private void resizeForPreviewAspectRatio() { 665 mPreviewFrameLayout.setAspectRatio( 666 (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); 667 } 668 669 @Override 670 protected void onResume() { 671 super.onResume(); 672 mPausing = false; 673 674 mVideoPreview.setVisibility(View.VISIBLE); 675 readVideoPreferences(); 676 resizeForPreviewAspectRatio(); 677 if (!mPreviewing && !mStartPreviewFail) { 678 if (!restartPreview()) return; 679 } 680 keepScreenOnAwhile(); 681 682 // install an intent filter to receive SD card related events. 683 IntentFilter intentFilter = 684 new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); 685 intentFilter.addAction(Intent.ACTION_MEDIA_EJECT); 686 intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 687 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 688 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 689 intentFilter.addDataScheme("file"); 690 mReceiver = new MyBroadcastReceiver(); 691 registerReceiver(mReceiver, intentFilter); 692 mStorageStatus = getStorageStatus(true); 693 694 mHandler.postDelayed(new Runnable() { 695 public void run() { 696 showStorageHint(); 697 } 698 }, 200); 699 700 if (mSurfaceHolder != null) { 701 mHandler.sendEmptyMessage(INIT_RECORDER); 702 } 703 704 changeHeadUpDisplayState(); 705 } 706 707 private void setPreviewDisplay(SurfaceHolder holder) { 708 try { 709 mCameraDevice.setPreviewDisplay(holder); 710 } catch (Throwable ex) { 711 closeCamera(); 712 throw new RuntimeException("setPreviewDisplay failed", ex); 713 } 714 } 715 716 private void startPreview() throws CameraHardwareException { 717 Log.v(TAG, "startPreview"); 718 if (mCameraDevice == null) { 719 // If the activity is paused and resumed, camera device has been 720 // released and we need to open the camera. 721 mCameraDevice = CameraHolder.instance().open(mCameraId); 722 Util.setCameraDisplayOrientation(this, mCameraId, mCameraDevice); 723 } 724 725 mCameraDevice.lock(); 726 setCameraParameters(); 727 setPreviewDisplay(mSurfaceHolder); 728 729 try { 730 mCameraDevice.startPreview(); 731 mPreviewing = true; 732 } catch (Throwable ex) { 733 closeCamera(); 734 throw new RuntimeException("startPreview failed", ex); 735 } 736 737 // If setPreviewDisplay has been set with a valid surface, unlock now. 738 // If surface is null, unlock later. Otherwise, setPreviewDisplay in 739 // surfaceChanged will fail. 740 if (mSurfaceHolder != null) { 741 mCameraDevice.unlock(); 742 } 743 } 744 745 private void closeCamera() { 746 Log.v(TAG, "closeCamera"); 747 if (mCameraDevice == null) { 748 Log.d(TAG, "already stopped."); 749 return; 750 } 751 // If we don't lock the camera, release() will fail. 752 mCameraDevice.lock(); 753 CameraHolder.instance().release(); 754 mCameraDevice = null; 755 mPreviewing = false; 756 } 757 758 private void finishRecorderAndCloseCamera() { 759 // This is similar to what mShutterButton.performClick() does, 760 // but not quite the same. 761 if (mMediaRecorderRecording) { 762 if (mIsVideoCaptureIntent) { 763 stopVideoRecording(); 764 showAlert(); 765 } else { 766 stopVideoRecordingAndGetThumbnail(); 767 } 768 } else { 769 stopVideoRecording(); 770 } 771 closeCamera(); 772 } 773 774 @Override 775 protected void onPause() { 776 super.onPause(); 777 mPausing = true; 778 779 changeHeadUpDisplayState(); 780 781 // Hide the preview now. Otherwise, the preview may be rotated during 782 // onPause and it is annoying to users. 783 mVideoPreview.setVisibility(View.INVISIBLE); 784 785 finishRecorderAndCloseCamera(); 786 787 if (mReceiver != null) { 788 unregisterReceiver(mReceiver); 789 mReceiver = null; 790 } 791 resetScreenOn(); 792 793 if (!mIsVideoCaptureIntent) { 794 mThumbController.storeData(ImageManager.getLastVideoThumbPath()); 795 } 796 797 if (mStorageHint != null) { 798 mStorageHint.cancel(); 799 mStorageHint = null; 800 } 801 802 mHandler.removeMessages(INIT_RECORDER); 803 804 } 805 806 @Override 807 public void onUserInteraction() { 808 super.onUserInteraction(); 809 if (!mMediaRecorderRecording) keepScreenOnAwhile(); 810 } 811 812 @Override 813 public void onBackPressed() { 814 if (mPausing) return; 815 if (mMediaRecorderRecording) { 816 onStopVideoRecording(false); 817 } else if (mHeadUpDisplay == null || !mHeadUpDisplay.collapse()) { 818 super.onBackPressed(); 819 } 820 } 821 822 @Override 823 public boolean onKeyDown(int keyCode, KeyEvent event) { 824 // Do not handle any key if the activity is paused. 825 if (mPausing) { 826 return true; 827 } 828 829 switch (keyCode) { 830 case KeyEvent.KEYCODE_CAMERA: 831 if (event.getRepeatCount() == 0) { 832 mShutterButton.performClick(); 833 return true; 834 } 835 break; 836 case KeyEvent.KEYCODE_DPAD_CENTER: 837 if (event.getRepeatCount() == 0) { 838 mShutterButton.performClick(); 839 return true; 840 } 841 break; 842 case KeyEvent.KEYCODE_MENU: 843 if (mMediaRecorderRecording) { 844 onStopVideoRecording(true); 845 return true; 846 } 847 break; 848 } 849 850 return super.onKeyDown(keyCode, event); 851 } 852 853 @Override 854 public boolean onKeyUp(int keyCode, KeyEvent event) { 855 switch (keyCode) { 856 case KeyEvent.KEYCODE_CAMERA: 857 mShutterButton.setPressed(false); 858 return true; 859 } 860 return super.onKeyUp(keyCode, event); 861 } 862 863 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { 864 // Make sure we have a surface in the holder before proceeding. 865 if (holder.getSurface() == null) { 866 Log.d(TAG, "holder.getSurface() == null"); 867 return; 868 } 869 870 mSurfaceHolder = holder; 871 872 if (mPausing) { 873 // We're pausing, the screen is off and we already stopped 874 // video recording. We don't want to start the camera again 875 // in this case in order to conserve power. 876 // The fact that surfaceChanged is called _after_ an onPause appears 877 // to be legitimate since in that case the lockscreen always returns 878 // to portrait orientation possibly triggering the notification. 879 return; 880 } 881 882 // The mCameraDevice will be null if it is fail to connect to the 883 // camera hardware. In this case we will show a dialog and then 884 // finish the activity, so it's OK to ignore it. 885 if (mCameraDevice == null) return; 886 887 // Set preview display if the surface is being created. Preview was 888 // already started. 889 if (holder.isCreating()) { 890 setPreviewDisplay(holder); 891 mCameraDevice.unlock(); 892 mHandler.sendEmptyMessage(INIT_RECORDER); 893 } else { 894 stopVideoRecording(); 895 restartPreview(); 896 initializeRecorder(); 897 } 898 } 899 900 public void surfaceCreated(SurfaceHolder holder) { 901 } 902 903 public void surfaceDestroyed(SurfaceHolder holder) { 904 mSurfaceHolder = null; 905 } 906 907 private void gotoGallery() { 908 MenuHelper.gotoCameraVideoGallery(this); 909 } 910 911 @Override 912 public boolean onCreateOptionsMenu(Menu menu) { 913 super.onCreateOptionsMenu(menu); 914 915 if (mIsVideoCaptureIntent) { 916 // No options menu for attach mode. 917 return false; 918 } else { 919 addBaseMenuItems(menu); 920 } 921 return true; 922 } 923 924 private boolean isVideoCaptureIntent() { 925 String action = getIntent().getAction(); 926 return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action)); 927 } 928 929 private void doReturnToCaller(boolean valid) { 930 Intent resultIntent = new Intent(); 931 int resultCode; 932 if (valid) { 933 resultCode = RESULT_OK; 934 resultIntent.setData(mCurrentVideoUri); 935 } else { 936 resultCode = RESULT_CANCELED; 937 } 938 setResult(resultCode, resultIntent); 939 finish(); 940 } 941 942 /** 943 * Returns 944 * 945 * @return number of bytes available, or an ERROR code. 946 */ 947 private static long getAvailableStorage() { 948 try { 949 if (!ImageManager.hasStorage()) { 950 return NO_STORAGE_ERROR; 951 } else { 952 String storageDirectory = 953 Environment.getExternalStorageDirectory().toString(); 954 StatFs stat = new StatFs(storageDirectory); 955 return (long) stat.getAvailableBlocks() 956 * (long) stat.getBlockSize(); 957 } 958 } catch (Exception ex) { 959 // if we can't stat the filesystem then we don't know how many 960 // free bytes exist. It might be zero but just leave it 961 // blank since we really don't know. 962 Log.e(TAG, "Fail to access sdcard", ex); 963 return CANNOT_STAT_ERROR; 964 } 965 } 966 967 private void cleanupEmptyFile() { 968 if (mVideoFilename != null) { 969 File f = new File(mVideoFilename); 970 if (f.length() == 0 && f.delete()) { 971 Log.v(TAG, "Empty video file deleted: " + mVideoFilename); 972 mVideoFilename = null; 973 } 974 } 975 } 976 977 private android.hardware.Camera mCameraDevice; 978 979 // Prepares media recorder. 980 private void initializeRecorder() { 981 Log.v(TAG, "initializeRecorder"); 982 if (mMediaRecorder != null) return; 983 984 // We will call initializeRecorder() again when the alert is hidden. 985 // If the mCameraDevice is null, then this activity is going to finish 986 if (isAlertVisible() || mCameraDevice == null) return; 987 988 Intent intent = getIntent(); 989 Bundle myExtras = intent.getExtras(); 990 991 long requestedSizeLimit = 0; 992 if (mIsVideoCaptureIntent && myExtras != null) { 993 Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); 994 if (saveUri != null) { 995 try { 996 mVideoFileDescriptor = 997 mContentResolver.openFileDescriptor(saveUri, "rw"); 998 mCurrentVideoUri = saveUri; 999 } catch (java.io.FileNotFoundException ex) { 1000 // invalid uri 1001 Log.e(TAG, ex.toString()); 1002 } 1003 } 1004 requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); 1005 } 1006 mMediaRecorder = new MediaRecorder(); 1007 1008 mMediaRecorder.setCamera(mCameraDevice); 1009 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 1010 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 1011 mMediaRecorder.setProfile(mProfile); 1012 if (mMaxVideoDurationInMs != 0) { 1013 mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs); 1014 } 1015 if (mCaptureTimeLapse) { 1016 mMediaRecorder.setTimeLapseParameters(mCaptureTimeLapse, mUseStillCameraForTimeLapse, 1017 mTimeBetweenTimeLapseFrameCaptureMs, mEncoderLevel); 1018 } 1019 1020 // Set output file. 1021 if (mStorageStatus != STORAGE_STATUS_OK) { 1022 mMediaRecorder.setOutputFile("/dev/null"); 1023 } else { 1024 // Try Uri in the intent first. If it doesn't exist, use our own 1025 // instead. 1026 if (mVideoFileDescriptor != null) { 1027 mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); 1028 try { 1029 mVideoFileDescriptor.close(); 1030 } catch (IOException e) { 1031 Log.e(TAG, "Fail to close fd", e); 1032 } 1033 } else { 1034 createVideoPath(); 1035 mMediaRecorder.setOutputFile(mVideoFilename); 1036 } 1037 } 1038 1039 mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 1040 1041 // Set maximum file size. 1042 // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter 1043 // of that to make it more likely that recording can complete 1044 // successfully. 1045 long maxFileSize = getAvailableStorage() - LOW_STORAGE_THRESHOLD / 4; 1046 if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { 1047 maxFileSize = requestedSizeLimit; 1048 } 1049 1050 try { 1051 mMediaRecorder.setMaxFileSize(maxFileSize); 1052 } catch (RuntimeException exception) { 1053 // We are going to ignore failure of setMaxFileSize here, as 1054 // a) The composer selected may simply not support it, or 1055 // b) The underlying media framework may not handle 64-bit range 1056 // on the size restriction. 1057 } 1058 1059 try { 1060 mMediaRecorder.prepare(); 1061 } catch (IOException e) { 1062 Log.e(TAG, "prepare failed for " + mVideoFilename); 1063 releaseMediaRecorder(); 1064 throw new RuntimeException(e); 1065 } 1066 mMediaRecorderRecording = false; 1067 1068 // Update the last video thumbnail. 1069 if (!mIsVideoCaptureIntent) { 1070 if (!mThumbController.isUriValid()) { 1071 updateLastVideo(); 1072 } 1073 mThumbController.updateDisplayIfNeeded(); 1074 } 1075 } 1076 1077 private void releaseMediaRecorder() { 1078 Log.v(TAG, "Releasing media recorder."); 1079 if (mMediaRecorder != null) { 1080 cleanupEmptyFile(); 1081 mMediaRecorder.reset(); 1082 mMediaRecorder.release(); 1083 mMediaRecorder = null; 1084 } 1085 } 1086 1087 private void createVideoPath() { 1088 long dateTaken = System.currentTimeMillis(); 1089 String title = createName(dateTaken); 1090 String filename = title + ".3gp"; // Used when emailing. 1091 String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME; 1092 String filePath = cameraDirPath + "/" + filename; 1093 File cameraDir = new File(cameraDirPath); 1094 cameraDir.mkdirs(); 1095 ContentValues values = new ContentValues(7); 1096 values.put(Video.Media.TITLE, title); 1097 values.put(Video.Media.DISPLAY_NAME, filename); 1098 values.put(Video.Media.DATE_TAKEN, dateTaken); 1099 values.put(Video.Media.MIME_TYPE, "video/3gpp"); 1100 values.put(Video.Media.DATA, filePath); 1101 mVideoFilename = filePath; 1102 Log.v(TAG, "Current camera video filename: " + mVideoFilename); 1103 mCurrentVideoValues = values; 1104 } 1105 1106 private void registerVideo() { 1107 if (mVideoFileDescriptor == null) { 1108 Uri videoTable = Uri.parse("content://media/external/video/media"); 1109 mCurrentVideoValues.put(Video.Media.SIZE, 1110 new File(mCurrentVideoFilename).length()); 1111 try { 1112 mCurrentVideoUri = mContentResolver.insert(videoTable, 1113 mCurrentVideoValues); 1114 } catch (Exception e) { 1115 // We failed to insert into the database. This can happen if 1116 // the SD card is unmounted. 1117 mCurrentVideoUri = null; 1118 mCurrentVideoFilename = null; 1119 } finally { 1120 Log.v(TAG, "Current video URI: " + mCurrentVideoUri); 1121 } 1122 } 1123 mCurrentVideoValues = null; 1124 } 1125 1126 private void deleteCurrentVideo() { 1127 if (mCurrentVideoFilename != null) { 1128 deleteVideoFile(mCurrentVideoFilename); 1129 mCurrentVideoFilename = null; 1130 } 1131 if (mCurrentVideoUri != null) { 1132 mContentResolver.delete(mCurrentVideoUri, null, null); 1133 mCurrentVideoUri = null; 1134 } 1135 updateAndShowStorageHint(true); 1136 } 1137 1138 private void deleteVideoFile(String fileName) { 1139 Log.v(TAG, "Deleting video " + fileName); 1140 File f = new File(fileName); 1141 if (!f.delete()) { 1142 Log.v(TAG, "Could not delete " + fileName); 1143 } 1144 } 1145 1146 private void setTimeLapseSwitchTitle(boolean enableTimeLapse) { 1147 int labelId = enableTimeLapse 1148 ? R.string.enable_time_lapse_mode 1149 : R.string.disable_time_lapse_mode; 1150 1151 mSwitchTimeLapseMenuItem.setTitle(labelId); 1152 } 1153 1154 private void addBaseMenuItems(Menu menu) { 1155 MenuHelper.addSwitchModeMenuItem(menu, false, new Runnable() { 1156 public void run() { 1157 switchToCameraMode(); 1158 } 1159 }); 1160 MenuItem gallery = menu.add(Menu.NONE, Menu.NONE, 1161 MenuHelper.POSITION_GOTO_GALLERY, 1162 R.string.camera_gallery_photos_text) 1163 .setOnMenuItemClickListener( 1164 new OnMenuItemClickListener() { 1165 public boolean onMenuItemClick(MenuItem item) { 1166 gotoGallery(); 1167 return true; 1168 } 1169 }); 1170 gallery.setIcon(android.R.drawable.ic_menu_gallery); 1171 mGalleryItems.add(gallery); 1172 1173 if (mNumberOfCameras > 1) { 1174 menu.add(Menu.NONE, Menu.NONE, 1175 MenuHelper.POSITION_SWITCH_CAMERA_ID, 1176 R.string.switch_camera_id) 1177 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 1178 public boolean onMenuItemClick(MenuItem item) { 1179 switchCameraId(); 1180 return true; 1181 } 1182 }).setIcon(android.R.drawable.ic_menu_camera); 1183 } 1184 1185 mSwitchTimeLapseMenuItem = menu.add(Menu.NONE, Menu.NONE, 1186 MenuHelper.POSITION_SWITCH_TIME_LAPSE_MODE, 1187 R.string.enable_time_lapse_mode) 1188 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 1189 public boolean onMenuItemClick(MenuItem item) { 1190 switchTimeLapseMode(); 1191 return true; 1192 } 1193 }).setIcon(android.R.drawable.ic_menu_camera); 1194 } 1195 1196 private void switchTimeLapseMode() { 1197 mCaptureTimeLapse = !mCaptureTimeLapse; 1198 1199 mSwitching = true; 1200 changeHeadUpDisplayState(); 1201 1202 finishRecorderAndCloseCamera(); 1203 mHandler.removeMessages(INIT_RECORDER); 1204 1205 // Read the video preferences 1206 readVideoPreferences(); 1207 resetCameraParameters(); 1208 1209 // Restart preview 1210 try { 1211 startPreview(); 1212 } catch (CameraHardwareException e) { 1213 showCameraBusyAndFinish(); 1214 return; 1215 } 1216 1217 // Reload the UI. 1218 initializeHeadUpDisplay(); 1219 1220 if (mSurfaceHolder != null) { 1221 mHandler.sendEmptyMessage(INIT_RECORDER); 1222 } 1223 1224 mSwitching = false; 1225 changeHeadUpDisplayState(); 1226 1227 // Change menu 1228 setTimeLapseSwitchTitle(!mCaptureTimeLapse); 1229 } 1230 1231 private void switchCameraId() { 1232 mSwitching = true; 1233 1234 mCameraId = (mCameraId + 1) % mNumberOfCameras; 1235 CameraSettings.writePreferredCameraId(mPreferences, mCameraId); 1236 1237 changeHeadUpDisplayState(); 1238 1239 finishRecorderAndCloseCamera(); 1240 mHandler.removeMessages(INIT_RECORDER); 1241 1242 // Reload the preferences. 1243 mPreferences.setLocalId(this, mCameraId); 1244 CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); 1245 1246 restartPreview(); 1247 1248 // Reload the UI. 1249 initializeHeadUpDisplay(); 1250 1251 if (mSurfaceHolder != null) { 1252 mHandler.sendEmptyMessage(INIT_RECORDER); 1253 } 1254 1255 mSwitching = false; 1256 changeHeadUpDisplayState(); 1257 } 1258 1259 private PreferenceGroup filterPreferenceScreenByIntent( 1260 PreferenceGroup screen) { 1261 Intent intent = getIntent(); 1262 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 1263 CameraSettings.removePreferenceFromScreen(screen, 1264 CameraSettings.KEY_VIDEO_QUALITY); 1265 } 1266 1267 if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { 1268 CameraSettings.removePreferenceFromScreen(screen, 1269 CameraSettings.KEY_VIDEO_QUALITY); 1270 } 1271 return screen; 1272 } 1273 1274 // from MediaRecorder.OnErrorListener 1275 public void onError(MediaRecorder mr, int what, int extra) { 1276 if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { 1277 // We may have run out of space on the sdcard. 1278 stopVideoRecording(); 1279 updateAndShowStorageHint(true); 1280 } 1281 } 1282 1283 // from MediaRecorder.OnInfoListener 1284 public void onInfo(MediaRecorder mr, int what, int extra) { 1285 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { 1286 if (mMediaRecorderRecording) onStopVideoRecording(true); 1287 } else if (what 1288 == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 1289 if (mMediaRecorderRecording) onStopVideoRecording(true); 1290 1291 // Show the toast. 1292 Toast.makeText(VideoCamera.this, R.string.video_reach_size_limit, 1293 Toast.LENGTH_LONG).show(); 1294 } 1295 } 1296 1297 /* 1298 * Make sure we're not recording music playing in the background, ask the 1299 * MediaPlaybackService to pause playback. 1300 */ 1301 private void pauseAudioPlayback() { 1302 // Shamelessly copied from MediaPlaybackService.java, which 1303 // should be public, but isn't. 1304 Intent i = new Intent("com.android.music.musicservicecommand"); 1305 i.putExtra("command", "pause"); 1306 1307 sendBroadcast(i); 1308 } 1309 1310 private void startVideoRecording() { 1311 Log.v(TAG, "startVideoRecording"); 1312 if (!mMediaRecorderRecording) { 1313 1314 if (mStorageStatus != STORAGE_STATUS_OK) { 1315 Log.v(TAG, "Storage issue, ignore the start request"); 1316 return; 1317 } 1318 1319 // Check mMediaRecorder to see whether it is initialized or not. 1320 if (mMediaRecorder == null) { 1321 Log.e(TAG, "MediaRecorder is not initialized."); 1322 return; 1323 } 1324 1325 pauseAudioPlayback(); 1326 1327 try { 1328 mMediaRecorder.setOnErrorListener(this); 1329 mMediaRecorder.setOnInfoListener(this); 1330 mMediaRecorder.start(); // Recording is now started 1331 } catch (RuntimeException e) { 1332 Log.e(TAG, "Could not start media recorder. ", e); 1333 return; 1334 } 1335 mHeadUpDisplay.setEnabled(false); 1336 1337 mMediaRecorderRecording = true; 1338 mRecordingStartTime = SystemClock.uptimeMillis(); 1339 updateRecordingIndicator(false); 1340 mRecordingTimeView.setText(""); 1341 mRecordingTimeView.setVisibility(View.VISIBLE); 1342 updateRecordingTime(); 1343 keepScreenOn(); 1344 } 1345 } 1346 1347 private void updateRecordingIndicator(boolean showRecording) { 1348 int drawableId = 1349 showRecording ? R.drawable.btn_ic_video_record 1350 : R.drawable.btn_ic_video_record_stop; 1351 Drawable drawable = getResources().getDrawable(drawableId); 1352 mShutterButton.setImageDrawable(drawable); 1353 } 1354 1355 private void stopVideoRecordingAndGetThumbnail() { 1356 stopVideoRecording(); 1357 acquireVideoThumb(); 1358 } 1359 1360 private void stopVideoRecordingAndReturn(boolean valid) { 1361 stopVideoRecording(); 1362 doReturnToCaller(valid); 1363 } 1364 1365 private void stopVideoRecordingAndShowAlert() { 1366 stopVideoRecording(); 1367 showAlert(); 1368 } 1369 1370 private void showAlert() { 1371 fadeOut(findViewById(R.id.shutter_button)); 1372 if (mCurrentVideoFilename != null) { 1373 mVideoFrame.setImageBitmap( 1374 ThumbnailUtils.createVideoThumbnail(mCurrentVideoFilename, 1375 Video.Thumbnails.MINI_KIND)); 1376 mVideoFrame.setVisibility(View.VISIBLE); 1377 } 1378 int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play}; 1379 for (int id : pickIds) { 1380 View button = findViewById(id); 1381 fadeIn(((View) button.getParent())); 1382 } 1383 } 1384 1385 private void hideAlert() { 1386 mVideoFrame.setVisibility(View.INVISIBLE); 1387 fadeIn(findViewById(R.id.shutter_button)); 1388 int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play}; 1389 for (int id : pickIds) { 1390 View button = findViewById(id); 1391 fadeOut(((View) button.getParent())); 1392 } 1393 } 1394 1395 private static void fadeIn(View view) { 1396 view.setVisibility(View.VISIBLE); 1397 Animation animation = new AlphaAnimation(0F, 1F); 1398 animation.setDuration(500); 1399 view.startAnimation(animation); 1400 } 1401 1402 private static void fadeOut(View view) { 1403 view.setVisibility(View.INVISIBLE); 1404 Animation animation = new AlphaAnimation(1F, 0F); 1405 animation.setDuration(500); 1406 view.startAnimation(animation); 1407 } 1408 1409 private boolean isAlertVisible() { 1410 return this.mVideoFrame.getVisibility() == View.VISIBLE; 1411 } 1412 1413 private void viewLastVideo() { 1414 Intent intent = null; 1415 if (mThumbController.isUriValid()) { 1416 intent = new Intent(Util.REVIEW_ACTION, mThumbController.getUri()); 1417 try { 1418 startActivity(intent); 1419 } catch (ActivityNotFoundException ex) { 1420 try { 1421 intent = new Intent(Intent.ACTION_VIEW, mThumbController.getUri()); 1422 } catch (ActivityNotFoundException e) { 1423 Log.e(TAG, "review video fail", e); 1424 } 1425 } 1426 } else { 1427 Log.e(TAG, "Can't view last video."); 1428 } 1429 } 1430 1431 private void stopVideoRecording() { 1432 Log.v(TAG, "stopVideoRecording"); 1433 boolean needToRegisterRecording = false; 1434 if (mMediaRecorderRecording || mMediaRecorder != null) { 1435 if (mMediaRecorderRecording && mMediaRecorder != null) { 1436 try { 1437 mMediaRecorder.setOnErrorListener(null); 1438 mMediaRecorder.setOnInfoListener(null); 1439 mMediaRecorder.stop(); 1440 } catch (RuntimeException e) { 1441 Log.e(TAG, "stop fail: " + e.getMessage()); 1442 } 1443 mHeadUpDisplay.setEnabled(true); 1444 mCurrentVideoFilename = mVideoFilename; 1445 Log.v(TAG, "Setting current video filename: " 1446 + mCurrentVideoFilename); 1447 needToRegisterRecording = true; 1448 mMediaRecorderRecording = false; 1449 } 1450 releaseMediaRecorder(); 1451 updateRecordingIndicator(true); 1452 mRecordingTimeView.setVisibility(View.GONE); 1453 keepScreenOnAwhile(); 1454 } 1455 if (needToRegisterRecording && mStorageStatus == STORAGE_STATUS_OK) { 1456 registerVideo(); 1457 } 1458 1459 mVideoFilename = null; 1460 mVideoFileDescriptor = null; 1461 } 1462 1463 private void resetScreenOn() { 1464 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1465 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1466 } 1467 1468 private void keepScreenOnAwhile() { 1469 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1470 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1471 mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1472 } 1473 1474 private void keepScreenOn() { 1475 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1476 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1477 } 1478 1479 private void hideAlertAndInitializeRecorder() { 1480 hideAlert(); 1481 mHandler.sendEmptyMessage(INIT_RECORDER); 1482 } 1483 1484 private void acquireVideoThumb() { 1485 Bitmap videoFrame = ThumbnailUtils.createVideoThumbnail( 1486 mCurrentVideoFilename, Video.Thumbnails.MINI_KIND); 1487 mThumbController.setData(mCurrentVideoUri, videoFrame); 1488 } 1489 1490 private static ImageManager.DataLocation dataLocation() { 1491 return ImageManager.DataLocation.EXTERNAL; 1492 } 1493 1494 private void updateLastVideo() { 1495 IImageList list = ImageManager.makeImageList( 1496 mContentResolver, 1497 dataLocation(), 1498 ImageManager.INCLUDE_VIDEOS, 1499 ImageManager.SORT_ASCENDING, 1500 ImageManager.CAMERA_IMAGE_BUCKET_ID); 1501 int count = list.getCount(); 1502 if (count > 0) { 1503 IImage image = list.getImageAt(count - 1); 1504 Uri uri = image.fullSizeImageUri(); 1505 mThumbController.setData(uri, image.miniThumbBitmap()); 1506 } else { 1507 mThumbController.setData(null, null); 1508 } 1509 list.close(); 1510 } 1511 1512 private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) { 1513 long seconds = milliSeconds / 1000; // round down to compute seconds 1514 long minutes = seconds / 60; 1515 long hours = minutes / 60; 1516 long remainderMinutes = minutes - (hours * 60); 1517 long remainderSeconds = seconds - (minutes * 60); 1518 1519 StringBuilder timeStringBuilder = new StringBuilder(); 1520 1521 // Hours 1522 if (hours > 0) { 1523 if (hours < 10) { 1524 timeStringBuilder.append('0'); 1525 } 1526 timeStringBuilder.append(hours); 1527 1528 timeStringBuilder.append(':'); 1529 } 1530 1531 // Minutes 1532 if (remainderMinutes < 10) { 1533 timeStringBuilder.append('0'); 1534 } 1535 timeStringBuilder.append(remainderMinutes); 1536 timeStringBuilder.append(':'); 1537 1538 // Seconds 1539 if (remainderSeconds < 10) { 1540 timeStringBuilder.append('0'); 1541 } 1542 timeStringBuilder.append(remainderSeconds); 1543 1544 // Centi seconds 1545 if (displayCentiSeconds) { 1546 timeStringBuilder.append('.'); 1547 long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10; 1548 if (remainderCentiSeconds < 10) { 1549 timeStringBuilder.append('0'); 1550 } 1551 timeStringBuilder.append(remainderCentiSeconds); 1552 } 1553 1554 return timeStringBuilder.toString(); 1555 } 1556 1557 // Calculates the time lapse video length till now and returns it in 1558 // the format hh:mm:ss.dd, where dd are the centi seconds. 1559 private String getTimeLapseVideoLengthString(long deltaMs) { 1560 // For better approximation calculate fractional number of frames captured. 1561 // This will update the video time at a higher resolution. 1562 double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs; 1563 long videoTimeMs = 1564 (long) (numberOfFrames / (double) mProfile.videoFrameRate * 1000); 1565 return millisecondToTimeString(videoTimeMs, true); 1566 } 1567 1568 private void updateRecordingTime() { 1569 if (!mMediaRecorderRecording) { 1570 return; 1571 } 1572 long now = SystemClock.uptimeMillis(); 1573 long delta = now - mRecordingStartTime; 1574 1575 // Starting a minute before reaching the max duration 1576 // limit, we'll countdown the remaining time instead. 1577 boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0 1578 && delta >= mMaxVideoDurationInMs - 60000); 1579 1580 long deltaAdjusted = delta; 1581 if (countdownRemainingTime) { 1582 deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999; 1583 } 1584 String text = millisecondToTimeString(deltaAdjusted, false); 1585 1586 if (mCaptureTimeLapse) { 1587 // Since the length of time lapse video is different from the length 1588 // of the actual wall clock time elapsed, we display the video length 1589 // alongside the wall clock time. 1590 text = text + " (" + getTimeLapseVideoLengthString(delta) + ")"; 1591 } 1592 1593 mRecordingTimeView.setText(text); 1594 1595 if (mRecordingTimeCountsDown != countdownRemainingTime) { 1596 // Avoid setting the color on every update, do it only 1597 // when it needs changing. 1598 mRecordingTimeCountsDown = countdownRemainingTime; 1599 1600 int color = getResources().getColor(countdownRemainingTime 1601 ? R.color.recording_time_remaining_text 1602 : R.color.recording_time_elapsed_text); 1603 1604 mRecordingTimeView.setTextColor(color); 1605 } 1606 1607 long nextUpdateDelay = 1000 - (delta % 1000); 1608 mHandler.sendEmptyMessageDelayed( 1609 UPDATE_RECORD_TIME, nextUpdateDelay); 1610 } 1611 1612 private static boolean isSupported(String value, List<String> supported) { 1613 return supported == null ? false : supported.indexOf(value) >= 0; 1614 } 1615 1616 private void setCameraParameters() { 1617 mParameters = mCameraDevice.getParameters(); 1618 1619 mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); 1620 mParameters.setPreviewFrameRate(mProfile.videoFrameRate); 1621 1622 // Set flash mode. 1623 String flashMode = mPreferences.getString( 1624 CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, 1625 getString(R.string.pref_camera_video_flashmode_default)); 1626 List<String> supportedFlash = mParameters.getSupportedFlashModes(); 1627 if (isSupported(flashMode, supportedFlash)) { 1628 mParameters.setFlashMode(flashMode); 1629 } else { 1630 flashMode = mParameters.getFlashMode(); 1631 if (flashMode == null) { 1632 flashMode = getString( 1633 R.string.pref_camera_flashmode_no_flash); 1634 } 1635 } 1636 1637 // Set white balance parameter. 1638 String whiteBalance = mPreferences.getString( 1639 CameraSettings.KEY_WHITE_BALANCE, 1640 getString(R.string.pref_camera_whitebalance_default)); 1641 if (isSupported(whiteBalance, 1642 mParameters.getSupportedWhiteBalance())) { 1643 mParameters.setWhiteBalance(whiteBalance); 1644 } else { 1645 whiteBalance = mParameters.getWhiteBalance(); 1646 if (whiteBalance == null) { 1647 whiteBalance = Parameters.WHITE_BALANCE_AUTO; 1648 } 1649 } 1650 1651 // Set color effect parameter. 1652 String colorEffect = mPreferences.getString( 1653 CameraSettings.KEY_COLOR_EFFECT, 1654 getString(R.string.pref_camera_coloreffect_default)); 1655 if (isSupported(colorEffect, mParameters.getSupportedColorEffects())) { 1656 mParameters.setColorEffect(colorEffect); 1657 } 1658 1659 mCameraDevice.setParameters(mParameters); 1660 } 1661 1662 private boolean switchToCameraMode() { 1663 if (isFinishing() || mMediaRecorderRecording) return false; 1664 MenuHelper.gotoCameraMode(this); 1665 finish(); 1666 return true; 1667 } 1668 1669 public boolean onSwitchChanged(Switcher source, boolean onOff) { 1670 if (onOff == SWITCH_CAMERA) { 1671 return switchToCameraMode(); 1672 } else { 1673 return true; 1674 } 1675 } 1676 1677 @Override 1678 public void onConfigurationChanged(Configuration config) { 1679 super.onConfigurationChanged(config); 1680 1681 // If the camera resumes behind the lock screen, the orientation 1682 // will be portrait. That causes OOM when we try to allocation GPU 1683 // memory for the GLSurfaceView again when the orientation changes. So, 1684 // we delayed initialization of HeadUpDisplay until the orientation 1685 // becomes landscape. 1686 changeHeadUpDisplayState(); 1687 } 1688 1689 private void resetCameraParameters() { 1690 // We need to restart the preview if preview size is changed. 1691 Size size = mParameters.getPreviewSize(); 1692 if (size.width != mDesiredPreviewWidth 1693 || size.height != mDesiredPreviewHeight) { 1694 // It is assumed media recorder is released before 1695 // onSharedPreferenceChanged, so we can close the camera here. 1696 closeCamera(); 1697 resizeForPreviewAspectRatio(); 1698 restartPreview(); // Parameters will be set in startPreview(). 1699 } else { 1700 try { 1701 // We need to lock the camera before writing parameters. 1702 mCameraDevice.lock(); 1703 } catch (RuntimeException e) { 1704 // When preferences are added for the first time, this method 1705 // will be called. But OnScreenSetting is not displayed yet and 1706 // media recorder still owns the camera. Lock will fail and we 1707 // just ignore it. 1708 return; 1709 } 1710 setCameraParameters(); 1711 mCameraDevice.unlock(); 1712 } 1713 } 1714 1715 public void onSizeChanged() { 1716 // TODO: update the content on GLRootView 1717 } 1718 1719 private class MyHeadUpDisplayListener implements HeadUpDisplay.Listener { 1720 public void onSharedPreferencesChanged() { 1721 mHandler.post(new Runnable() { 1722 public void run() { 1723 VideoCamera.this.onSharedPreferencesChanged(); 1724 } 1725 }); 1726 } 1727 1728 public void onRestorePreferencesClicked() { 1729 mHandler.post(new Runnable() { 1730 public void run() { 1731 VideoCamera.this.onRestorePreferencesClicked(); 1732 } 1733 }); 1734 } 1735 1736 public void onPopupWindowVisibilityChanged(final int visibility) { 1737 mHandler.post(new Runnable() { 1738 public void run() { 1739 VideoCamera.this.onPopupWindowVisibilityChanged(visibility); 1740 } 1741 }); 1742 } 1743 } 1744 1745 private void onPopupWindowVisibilityChanged(int visibility) { 1746 if (visibility == GLView.VISIBLE) { 1747 releaseMediaRecorder(); 1748 } else { 1749 if (!mPausing && mSurfaceHolder != null) initializeRecorder(); 1750 } 1751 } 1752 1753 private void onRestorePreferencesClicked() { 1754 Runnable runnable = new Runnable() { 1755 public void run() { 1756 mHeadUpDisplay.restorePreferences(mParameters); 1757 } 1758 }; 1759 MenuHelper.confirmAction(this, 1760 getString(R.string.confirm_restore_title), 1761 getString(R.string.confirm_restore_message), 1762 runnable); 1763 } 1764 1765 private void onSharedPreferencesChanged() { 1766 // ignore the events after "onPause()" or preview has not started yet 1767 if (mPausing) return; 1768 synchronized (mPreferences) { 1769 readVideoPreferences(); 1770 // If mCameraDevice is not ready then we can set the parameter in 1771 // startPreview(). 1772 if (mCameraDevice == null) return; 1773 resetCameraParameters(); 1774 } 1775 } 1776} 1777