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