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