VideoCamera.java revision 37dcfbb99d78c5179ecda79e7fd408eca4d00844
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; 21 22import android.app.Activity; 23import android.content.ActivityNotFoundException; 24import android.content.BroadcastReceiver; 25import android.content.ContentResolver; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.SharedPreferences; 31import android.graphics.Bitmap; 32import android.graphics.drawable.Drawable; 33import android.media.MediaRecorder; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.Environment; 37import android.os.Handler; 38import android.os.Message; 39import android.os.StatFs; 40import android.os.SystemClock; 41import android.preference.PreferenceManager; 42import android.provider.MediaStore; 43import android.provider.MediaStore.Video; 44import android.text.format.DateFormat; 45import android.util.Log; 46import android.view.KeyEvent; 47import android.view.LayoutInflater; 48import android.view.Menu; 49import android.view.MenuItem; 50import android.view.SurfaceHolder; 51import android.view.View; 52import android.view.ViewGroup; 53import android.view.Window; 54import android.view.WindowManager; 55import android.view.MenuItem.OnMenuItemClickListener; 56import android.view.animation.AlphaAnimation; 57import android.view.animation.Animation; 58import android.widget.ImageView; 59import android.widget.TextView; 60import android.widget.Toast; 61 62import java.io.File; 63import java.io.FileDescriptor; 64import java.io.IOException; 65import java.text.SimpleDateFormat; 66import java.util.ArrayList; 67import java.util.Date; 68 69/** 70 * The Camcorder activity. 71 */ 72public class VideoCamera extends Activity implements View.OnClickListener, 73 ShutterButton.OnShutterButtonListener, SurfaceHolder.Callback, 74 MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener { 75 76 private static final String TAG = "videocamera"; 77 78 private static final int INIT_RECORDER = 3; 79 private static final int CLEAR_SCREEN_DELAY = 4; 80 private static final int UPDATE_RECORD_TIME = 5; 81 82 private static final int SCREEN_DELAY = 2 * 60 * 1000; 83 84 private static final long NO_STORAGE_ERROR = -1L; 85 private static final long CANNOT_STAT_ERROR = -2L; 86 private static final long LOW_STORAGE_THRESHOLD = 512L * 1024L; 87 88 private static final int STORAGE_STATUS_OK = 0; 89 private static final int STORAGE_STATUS_LOW = 1; 90 private static final int STORAGE_STATUS_NONE = 2; 91 92 public static final int MENU_SETTINGS = 6; 93 public static final int MENU_GALLERY_PHOTOS = 7; 94 public static final int MENU_GALLERY_VIDEOS = 8; 95 public static final int MENU_SAVE_GALLERY_PHOTO = 34; 96 public static final int MENU_SAVE_PLAY_VIDEO = 35; 97 public static final int MENU_SAVE_SELECT_VIDEO = 36; 98 public static final int MENU_SAVE_NEW_VIDEO = 37; 99 100 SharedPreferences mPreferences; 101 102 private static final float VIDEO_ASPECT_RATIO = 176.0f / 144.0f; 103 VideoPreview mVideoPreview; 104 SurfaceHolder mSurfaceHolder = null; 105 ImageView mVideoFrame; 106 107 private boolean mIsVideoCaptureIntent; 108 // mLastPictureButton and mThumbController 109 // are non-null only if mIsVideoCaptureIntent is true. 110 private ImageView mLastPictureButton; 111 private ThumbnailController mThumbController; 112 113 private int mStorageStatus = STORAGE_STATUS_OK; 114 115 private MediaRecorder mMediaRecorder; 116 private boolean mMediaRecorderRecording = false; 117 private long mRecordingStartTime; 118 // The video file that the hardware camera is about to record into 119 // (or is recording into.) 120 private String mCameraVideoFilename; 121 private FileDescriptor mCameraVideoFileDescriptor; 122 123 // The video file that has already been recorded, and that is being 124 // examined by the user. 125 private String mCurrentVideoFilename; 126 private Uri mCurrentVideoUri; 127 private ContentValues mCurrentVideoValues; 128 129 // The video frame size we will record (like 352x288). 130 private int mVideoWidth, mVideoHeight; 131 132 // The video duration limit. 133 private int mMaxVideoDurationInMs; 134 135 boolean mPausing = false; 136 boolean mPreviewing = false; // True if preview is started. 137 boolean mRecorderInitialized = false; 138 139 private ContentResolver mContentResolver; 140 141 private ShutterButton mShutterButton; 142 private TextView mRecordingTimeView; 143 private boolean mRecordingTimeCountsDown = false; 144 145 ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>(); 146 147 private final Handler mHandler = new MainHandler(); 148 149 // This Handler is used to post message back onto the main thread of the 150 // application 151 private class MainHandler extends Handler { 152 @Override 153 public void handleMessage(Message msg) { 154 switch (msg.what) { 155 156 case CLEAR_SCREEN_DELAY: { 157 clearScreenOnFlag(); 158 break; 159 } 160 161 case UPDATE_RECORD_TIME: { 162 updateRecordingTime(); 163 break; 164 } 165 166 case INIT_RECORDER: { 167 initializeRecorder(); 168 break; 169 } 170 171 default: 172 Log.v(TAG, "Unhandled message: " + msg.what); 173 break; 174 } 175 } 176 } 177 178 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 179 @Override 180 public void onReceive(Context context, Intent intent) { 181 String action = intent.getAction(); 182 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 183 updateAndShowStorageHint(false); 184 stopVideoRecording(); 185 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 186 updateAndShowStorageHint(true); 187 mRecorderInitialized = false; 188 initializeRecorder(); 189 } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 190 // SD card unavailable 191 // handled in ACTION_MEDIA_EJECT 192 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { 193 Toast.makeText(VideoCamera.this, getResources().getString(R.string.wait), 5000); 194 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { 195 updateAndShowStorageHint(true); 196 } 197 } 198 }; 199 200 private static String createName(long dateTaken) { 201 return DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString(); 202 } 203 204 /** Called with the activity is first created. */ 205 @Override 206 public void onCreate(Bundle icicle) { 207 super.onCreate(icicle); 208 209 /* 210 * To reduce startup time, we open camera device in another thread. 211 * Camera is opened in onCreate instead of onResume because there are 212 * lots of things to do here and camera open can be done in parallel. We 213 * will make sure the camera is opened at the end of onCreate. 214 */ 215 Thread openCameraThread = new Thread(new Runnable() { 216 public void run() { 217 mCameraDevice = CameraHolder.instance().open(); 218 } 219 }); 220 openCameraThread.start(); 221 222 mPreferences = PreferenceManager.getDefaultSharedPreferences(this); 223 mContentResolver = getContentResolver(); 224 225 requestWindowFeature(Window.FEATURE_PROGRESS); 226 setContentView(R.layout.video_camera); 227 228 mVideoPreview = (VideoPreview) findViewById(R.id.camera_preview); 229 mVideoPreview.setAspectRatio(VIDEO_ASPECT_RATIO); 230 231 // don't set mSurfaceHolder here. We have it set ONLY within 232 // surfaceCreated / surfaceDestroyed, other parts of the code 233 // assume that when it is set, the surface is also set. 234 SurfaceHolder holder = mVideoPreview.getHolder(); 235 holder.addCallback(this); 236 holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 237 238 mIsVideoCaptureIntent = isVideoCaptureIntent(); 239 mRecordingTimeView = (TextView) findViewById(R.id.recording_time); 240 mVideoFrame = (ImageView) findViewById(R.id.video_frame); 241 242 ViewGroup rootView = (ViewGroup) findViewById(R.id.video_camera); 243 LayoutInflater inflater = this.getLayoutInflater(); 244 if (!mIsVideoCaptureIntent) { 245 View controlBar = inflater.inflate(R.layout.camera_control, rootView); 246 mLastPictureButton = (ImageView) controlBar.findViewById(R.id.review_thumbnail); 247 mThumbController = new ThumbnailController(mLastPictureButton, mContentResolver); 248 mLastPictureButton.setOnClickListener(this); 249 mThumbController.loadData(ImageManager.getLastVideoThumbPath()); 250 findViewById(R.id.camera_switch).setOnClickListener(this); 251 } else { 252 View controlBar = inflater.inflate(R.layout.attach_camera_control, rootView); 253 controlBar.findViewById(R.id.btn_cancel).setOnClickListener(this); 254 controlBar.findViewById(R.id.btn_retake).setOnClickListener(this); 255 controlBar.findViewById(R.id.btn_play).setOnClickListener(this); 256 controlBar.findViewById(R.id.btn_done).setOnClickListener(this); 257 } 258 259 mShutterButton = (ShutterButton) findViewById(R.id.shutter_button); 260 mShutterButton.setImageResource(R.drawable.btn_ic_video_record); 261 mShutterButton.setOnShutterButtonListener(this); 262 mShutterButton.requestFocus(); 263 264 // Make sure the camera is opened. 265 try { 266 openCameraThread.join(); 267 } catch (InterruptedException ex) { 268 // ignore 269 } 270 } 271 272 private void startShareVideoActivity() { 273 Intent intent = new Intent(); 274 intent.setAction(Intent.ACTION_SEND); 275 intent.setType("video/3gpp"); 276 intent.putExtra(Intent.EXTRA_STREAM, mCurrentVideoUri); 277 try { 278 startActivity(Intent.createChooser(intent, getText(R.string.sendVideo))); 279 } catch (android.content.ActivityNotFoundException ex) { 280 Toast.makeText(VideoCamera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT) 281 .show(); 282 } 283 } 284 285 private void startPlayVideoActivity() { 286 Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri); 287 try { 288 startActivity(intent); 289 } catch (android.content.ActivityNotFoundException ex) { 290 Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); 291 } 292 } 293 294 295 public void onClick(View v) { 296 switch (v.getId()) { 297 case R.id.btn_retake: 298 discardCurrentVideoAndInitRecorder(); 299 break; 300 case R.id.camera_switch: 301 MenuHelper.gotoCameraMode(this); 302 break; 303 case R.id.btn_play: 304 startPlayVideoActivity(); 305 break; 306 case R.id.btn_done: 307 doReturnToCaller(true); 308 break; 309 case R.id.btn_cancel: 310 stopVideoRecording(); 311 doReturnToCaller(false); 312 break; 313 case R.id.discard: { 314 Runnable deleteCallback = new Runnable() { 315 public void run() { 316 discardCurrentVideoAndInitRecorder(); 317 } 318 }; 319 MenuHelper.deleteVideo(this, deleteCallback); 320 break; 321 } 322 case R.id.share: { 323 startShareVideoActivity(); 324 break; 325 } 326 case R.id.play: { 327 doPlayCurrentVideo(); 328 break; 329 } 330 case R.id.review_thumbnail: { 331 stopVideoRecordingAndShowReview(); 332 break; 333 } 334 } 335 } 336 337 public void onShutterButtonFocus(ShutterButton button, boolean pressed) { 338 // Do nothing (everything happens in onShutterButtonClick). 339 } 340 341 public void onShutterButtonClick(ShutterButton button) { 342 switch (button.getId()) { 343 case R.id.shutter_button: 344 if (mMediaRecorderRecording) { 345 if (mIsVideoCaptureIntent) { 346 stopVideoRecordingAndShowAlert(); 347 } else { 348 stopVideoRecordingAndGetThumbnail(); 349 mRecorderInitialized = false; 350 initializeRecorder(); 351 } 352 } else if (mRecorderInitialized) { 353 // If the click comes before recorder initialization, it is 354 // ignored. If users click the button during initialization, 355 // the event is put in the queue and record will be started 356 // eventually. 357 startVideoRecording(); 358 } 359 break; 360 } 361 } 362 363 private void doPlayCurrentVideo() { 364 Log.v(TAG, "Playing current video: " + mCurrentVideoUri); 365 Intent intent = new Intent(Intent.ACTION_VIEW, mCurrentVideoUri); 366 try { 367 startActivity(intent); 368 } catch (android.content.ActivityNotFoundException ex) { 369 Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); 370 } 371 } 372 373 private void discardCurrentVideoAndInitRecorder() { 374 deleteCurrentVideo(); 375 hideAlertAndInitializeRecorder(); 376 } 377 378 private OnScreenHint mStorageHint; 379 380 private void updateAndShowStorageHint(boolean mayHaveSd) { 381 mStorageStatus = getStorageStatus(mayHaveSd); 382 showStorageHint(); 383 } 384 385 private void showStorageHint() { 386 String errorMessage = null; 387 switch (mStorageStatus) { 388 case STORAGE_STATUS_NONE: 389 errorMessage = getString(R.string.no_storage); 390 break; 391 case STORAGE_STATUS_LOW: 392 errorMessage = getString(R.string.spaceIsLow_content); 393 } 394 if (errorMessage != null) { 395 if (mStorageHint == null) { 396 mStorageHint = OnScreenHint.makeText(this, errorMessage); 397 } else { 398 mStorageHint.setText(errorMessage); 399 } 400 mStorageHint.show(); 401 } else if (mStorageHint != null) { 402 mStorageHint.cancel(); 403 mStorageHint = null; 404 } 405 } 406 407 private int getStorageStatus(boolean mayHaveSd) { 408 long remaining = mayHaveSd ? getAvailableStorage() : NO_STORAGE_ERROR; 409 if (remaining == NO_STORAGE_ERROR) { 410 return STORAGE_STATUS_NONE; 411 } 412 return remaining < LOW_STORAGE_THRESHOLD ? STORAGE_STATUS_LOW : STORAGE_STATUS_OK; 413 } 414 415 private void readVideoSizePreference() { 416 boolean videoQualityHigh = 417 getBooleanPreference(CameraSettings.KEY_VIDEO_QUALITY, 418 CameraSettings.DEFAULT_VIDEO_QUALITY_VALUE); 419 420 // 1 minute = 60000ms 421 mMaxVideoDurationInMs = 422 60000 * getIntPreference(CameraSettings.KEY_VIDEO_DURATION, 423 CameraSettings.DEFAULT_VIDEO_DURATION_VALUE); 424 425 Intent intent = getIntent(); 426 if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { 427 int extraVideoQuality = intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 428 videoQualityHigh = (extraVideoQuality > 0); 429 } 430 431 if (videoQualityHigh) { 432 // CIF size 433 mVideoWidth = 352; 434 mVideoHeight = 288; 435 } else { 436 // QCIF size 437 mVideoWidth = 176; 438 mVideoHeight = 144; 439 } 440 } 441 442 @Override 443 public void onResume() { 444 super.onResume(); 445 mPausing = false; 446 447 setScreenTimeoutLong(); 448 readVideoSizePreference(); 449 450 // install an intent filter to receive SD card related events. 451 IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); 452 intentFilter.addAction(Intent.ACTION_MEDIA_EJECT); 453 intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 454 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 455 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 456 intentFilter.addDataScheme("file"); 457 registerReceiver(mReceiver, intentFilter); 458 mStorageStatus = getStorageStatus(true); 459 460 mHandler.postDelayed(new Runnable() { 461 public void run() { 462 showStorageHint(); 463 } 464 }, 200); 465 466 if (mSurfaceHolder != null) { 467 startPreview(); 468 mRecorderInitialized = false; 469 mHandler.sendEmptyMessage(INIT_RECORDER); 470 } 471 } 472 473 private void setCameraParameters() { 474 android.hardware.Camera.Parameters param; 475 param = mCameraDevice.getParameters(); 476 param.setPreviewSize(mVideoWidth, mVideoHeight); 477 mCameraDevice.setParameters(param); 478 } 479 480 // Precondition: mSurfaceHolder != null 481 private void startPreview() { 482 Log.v(TAG, "startPreview"); 483 if (mPreviewing) { 484 // After recording a video, preview is not stopped. So just return. 485 return; 486 } 487 488 if (mCameraDevice == null) { 489 // If the activity is paused and resumed, camera device has been 490 // released and we need to open the camera. 491 mCameraDevice = CameraHolder.instance().open(); 492 } 493 494 setCameraParameters(); 495 try { 496 mCameraDevice.setPreviewDisplay(mSurfaceHolder); 497 } catch (Throwable ex) { 498 closeCamera(); 499 throw new RuntimeException("setPreviewDisplay failed", ex); 500 } 501 502 try { 503 mCameraDevice.startPreview(); 504 mPreviewing = true; 505 } catch (Throwable ex) { 506 closeCamera(); 507 throw new RuntimeException("startPreview failed", ex); 508 } 509 mCameraDevice.unlock(); 510 } 511 512 private void closeCamera() { 513 Log.v(TAG, "closeCamera"); 514 if (mCameraDevice == null) { 515 Log.d(TAG, "already stopped."); 516 return; 517 } 518 // If we don't lock the camera, release() will fail. 519 mCameraDevice.lock(); 520 CameraHolder.instance().release(); 521 mCameraDevice = null; 522 mPreviewing = false; 523 } 524 525 @Override 526 public void onStop() { 527 setScreenTimeoutSystemDefault(); 528 super.onStop(); 529 } 530 531 @Override 532 protected void onPause() { 533 super.onPause(); 534 535 mPausing = true; 536 537 // This is similar to what mShutterButton.performClick() does, 538 // but not quite the same. 539 if (mMediaRecorderRecording) { 540 if (mIsVideoCaptureIntent) { 541 stopVideoRecording(); 542 showAlert(); 543 } else { 544 stopVideoRecordingAndGetThumbnail(); 545 } 546 } else { 547 stopVideoRecording(); 548 } 549 closeCamera(); 550 551 unregisterReceiver(mReceiver); 552 setScreenTimeoutSystemDefault(); 553 554 if (!mIsVideoCaptureIntent) { 555 mThumbController.storeData(ImageManager.getLastVideoThumbPath()); 556 } 557 558 if (mStorageHint != null) { 559 mStorageHint.cancel(); 560 mStorageHint = null; 561 } 562 563 mHandler.removeMessages(INIT_RECORDER); 564 } 565 566 @Override 567 public boolean onKeyDown(int keyCode, KeyEvent event) { 568 // Do not handle any key if the activity is paused. 569 if (mPausing) { 570 return true; 571 } 572 573 setScreenTimeoutLong(); 574 575 switch (keyCode) { 576 case KeyEvent.KEYCODE_BACK: 577 if (mMediaRecorderRecording) { 578 mShutterButton.performClick(); 579 return true; 580 } 581 break; 582 case KeyEvent.KEYCODE_CAMERA: 583 if (event.getRepeatCount() == 0) { 584 mShutterButton.performClick(); 585 return true; 586 } 587 break; 588 case KeyEvent.KEYCODE_DPAD_CENTER: 589 if (event.getRepeatCount() == 0) { 590 mShutterButton.performClick(); 591 return true; 592 } 593 break; 594 case KeyEvent.KEYCODE_MENU: 595 if (mMediaRecorderRecording) { 596 mShutterButton.performClick(); 597 return true; 598 } 599 break; 600 } 601 602 return super.onKeyDown(keyCode, event); 603 } 604 605 @Override 606 public boolean onKeyUp(int keyCode, KeyEvent event) { 607 switch (keyCode) { 608 case KeyEvent.KEYCODE_CAMERA: 609 mShutterButton.setPressed(false); 610 return true; 611 } 612 return super.onKeyUp(keyCode, event); 613 } 614 615 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { 616 // Make sure we have a surface in the holder before proceeding. 617 if (holder.getSurface() == null) { 618 Log.d(TAG, "holder.getSurface() == null"); 619 return; 620 } 621 622 if (mPausing) { 623 // We're pausing, the screen is off and we already stopped 624 // video recording. We don't want to start the camera again 625 // in this case in order to conserve power. 626 // The fact that surfaceChanged is called _after_ an onPause appears 627 // to be legitimate since in that case the lockscreen always returns 628 // to portrait orientation possibly triggering the notification. 629 return; 630 } 631 632 if (mMediaRecorderRecording) { 633 stopVideoRecording(); 634 } 635 636 // Start the preview if it is not started yet. Preview may be already 637 // started in onResume and then surfaceChanged is called due to 638 // orientation change. 639 if (!mPreviewing) { 640 startPreview(); 641 mRecorderInitialized = false; 642 mHandler.sendEmptyMessage(INIT_RECORDER); 643 } 644 } 645 646 public void surfaceCreated(SurfaceHolder holder) { 647 mSurfaceHolder = holder; 648 } 649 650 public void surfaceDestroyed(SurfaceHolder holder) { 651 mSurfaceHolder = null; 652 } 653 654 private void gotoGallery() { 655 MenuHelper.gotoCameraVideoGallery(this); 656 } 657 658 @Override 659 public boolean onPrepareOptionsMenu(Menu menu) { 660 super.onPrepareOptionsMenu(menu); 661 662 for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) { 663 if (i != MenuHelper.GENERIC_ITEM) { 664 menu.setGroupVisible(i, false); 665 } 666 } 667 668 menu.setGroupVisible(MenuHelper.VIDEO_MODE_ITEM, true); 669 return true; 670 } 671 672 @Override 673 public boolean onCreateOptionsMenu(Menu menu) { 674 super.onCreateOptionsMenu(menu); 675 676 if (mIsVideoCaptureIntent) { 677 // No options menu for attach mode. 678 return false; 679 } else { 680 addBaseMenuItems(menu); 681 int menuFlags = 682 MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU 683 & ~MenuHelper.INCLUDE_DETAILS_MENU; 684 MenuHelper.addImageMenuItems(menu, menuFlags, false, VideoCamera.this, mHandler, 685 // Handler for deletion 686 new Runnable() { 687 public void run() { 688 // What do we do here? 689 // mContentResolver.delete(uri, null, null); 690 } 691 }, new MenuHelper.MenuInvoker() { 692 public void run(final MenuHelper.MenuCallback cb) { 693 } 694 }); 695 696 MenuItem gallery = 697 menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0, 698 R.string.camera_gallery_photos_text).setOnMenuItemClickListener( 699 new MenuItem.OnMenuItemClickListener() { 700 public boolean onMenuItemClick(MenuItem item) { 701 gotoGallery(); 702 return true; 703 } 704 }); 705 gallery.setIcon(android.R.drawable.ic_menu_gallery); 706 } 707 return true; 708 } 709 710 private boolean isVideoCaptureIntent() { 711 String action = getIntent().getAction(); 712 return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action)); 713 } 714 715 private void doReturnToCaller(boolean success) { 716 Intent resultIntent = new Intent(); 717 int resultCode; 718 if (success) { 719 resultCode = RESULT_OK; 720 resultIntent.setData(mCurrentVideoUri); 721 } else { 722 resultCode = RESULT_CANCELED; 723 } 724 setResult(resultCode, resultIntent); 725 finish(); 726 } 727 728 /** 729 * Returns 730 * 731 * @return number of bytes available, or an ERROR code. 732 */ 733 private static long getAvailableStorage() { 734 try { 735 if (!ImageManager.hasStorage()) { 736 return NO_STORAGE_ERROR; 737 } else { 738 String storageDirectory = Environment.getExternalStorageDirectory().toString(); 739 StatFs stat = new StatFs(storageDirectory); 740 return ((long) stat.getAvailableBlocks() * (long) stat.getBlockSize()); 741 } 742 } catch (RuntimeException ex) { 743 // if we can't stat the filesystem then we don't know how many 744 // free bytes exist. It might be zero but just leave it 745 // blank since we really don't know. 746 return CANNOT_STAT_ERROR; 747 } 748 } 749 750 private void cleanupEmptyFile() { 751 if (mCameraVideoFilename != null) { 752 File f = new File(mCameraVideoFilename); 753 if (f.length() == 0 && f.delete()) { 754 Log.v(TAG, "Empty video file deleted: " + mCameraVideoFilename); 755 mCameraVideoFilename = null; 756 } 757 } 758 } 759 760 private android.hardware.Camera mCameraDevice; 761 762 // initializeRecorder() prepares media recorder. Return false if fails. 763 private boolean initializeRecorder() { 764 Log.v(TAG, "initializeRecorder"); 765 if (mRecorderInitialized) return true; 766 767 // We will call initializeRecorder() again when the alert is hidden. 768 if (isAlertVisible()) { 769 return false; 770 } 771 772 Intent intent = getIntent(); 773 Bundle myExtras = intent.getExtras(); 774 775 if (mIsVideoCaptureIntent && myExtras != null) { 776 Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); 777 if (saveUri != null) { 778 try { 779 mCameraVideoFileDescriptor = 780 mContentResolver.openFileDescriptor(saveUri, "rw").getFileDescriptor(); 781 mCurrentVideoUri = saveUri; 782 } catch (java.io.FileNotFoundException ex) { 783 // invalid uri 784 Log.e(TAG, ex.toString()); 785 } 786 } 787 } 788 releaseMediaRecorder(); 789 790 mMediaRecorder = new MediaRecorder(); 791 792 mMediaRecorder.setCamera(mCameraDevice); 793 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 794 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 795 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 796 mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs); 797 798 if (mStorageStatus != STORAGE_STATUS_OK) { 799 mMediaRecorder.setOutputFile("/dev/null"); 800 } else { 801 // We try Uri in intent first. If it doesn't work, use our own 802 // instead. 803 if (mCameraVideoFileDescriptor != null) { 804 mMediaRecorder.setOutputFile(mCameraVideoFileDescriptor); 805 } else { 806 createVideoPath(); 807 mMediaRecorder.setOutputFile(mCameraVideoFilename); 808 } 809 } 810 811 // Use the same frame rate for both, since internally 812 // if the frame rate is too large, it can cause camera to become 813 // unstable. We need to fix the MediaRecorder to disable the support 814 // of setting frame rate for now. 815 mMediaRecorder.setVideoFrameRate(20); 816 mMediaRecorder.setVideoSize(mVideoWidth, mVideoHeight); 817 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); 818 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 819 mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 820 821 long remaining = getAvailableStorage(); 822 // remaining >= LOW_STORAGE_THRESHOLD at this point, reserve a quarter 823 // of that to make it more likely that recording can complete 824 // successfully. 825 try { 826 mMediaRecorder.setMaxFileSize(remaining - LOW_STORAGE_THRESHOLD / 4); 827 } catch (RuntimeException exception) { 828 // We are going to ignore failure of setMaxFileSize here, as 829 // a) The composer selected may simply not support it, or 830 // b) The underlying media framework may not handle 64-bit range 831 // on the size restriction. 832 } 833 834 try { 835 mMediaRecorder.prepare(); 836 } catch (IOException exception) { 837 Log.e(TAG, "prepare failed for " + mCameraVideoFilename); 838 releaseMediaRecorder(); 839 // TODO: add more exception handling logic here 840 return false; 841 } 842 mMediaRecorderRecording = false; 843 844 // Update the last video thumbnail. 845 if (!mIsVideoCaptureIntent) { 846 if (!mThumbController.isUriValid()) { 847 updateLastVideo(); 848 } 849 mThumbController.updateDisplayIfNeeded(); 850 } 851 mRecorderInitialized = true; 852 return true; 853 } 854 855 private void releaseMediaRecorder() { 856 Log.v(TAG, "Releasing media recorder."); 857 if (mMediaRecorder != null) { 858 cleanupEmptyFile(); 859 mMediaRecorder.reset(); 860 mMediaRecorder.release(); 861 mMediaRecorder = null; 862 } 863 } 864 865 private int getIntPreference(String key, int defaultValue) { 866 String s = mPreferences.getString(key, ""); 867 int result = defaultValue; 868 try { 869 result = Integer.parseInt(s); 870 } catch (NumberFormatException e) { 871 // Ignore, result is already the default value. 872 } 873 return result; 874 } 875 876 private boolean getBooleanPreference(String key, boolean defaultValue) { 877 return getIntPreference(key, defaultValue ? 1 : 0) != 0; 878 } 879 880 private void createVideoPath() { 881 long dateTaken = System.currentTimeMillis(); 882 String title = createName(dateTaken); 883 String displayName = title + ".3gp"; // Used when emailing. 884 String cameraDirPath = ImageManager.CAMERA_IMAGE_BUCKET_NAME; 885 File cameraDir = new File(cameraDirPath); 886 cameraDir.mkdirs(); 887 SimpleDateFormat dateFormat = 888 new SimpleDateFormat(getString(R.string.video_file_name_format)); 889 Date date = new Date(dateTaken); 890 String filepart = dateFormat.format(date); 891 String filename = cameraDirPath + "/" + filepart + ".3gp"; 892 ContentValues values = new ContentValues(7); 893 values.put(Video.Media.TITLE, title); 894 values.put(Video.Media.DISPLAY_NAME, displayName); 895 values.put(Video.Media.DATE_TAKEN, dateTaken); 896 values.put(Video.Media.MIME_TYPE, "video/3gpp"); 897 values.put(Video.Media.DATA, filename); 898 mCameraVideoFilename = filename; 899 Log.v(TAG, "Current camera video filename: " + mCameraVideoFilename); 900 mCurrentVideoValues = values; 901 } 902 903 private void registerVideo() { 904 if (mCameraVideoFileDescriptor == null) { 905 Uri videoTable = Uri.parse("content://media/external/video/media"); 906 mCurrentVideoValues.put(Video.Media.SIZE, new File(mCurrentVideoFilename).length()); 907 mCurrentVideoUri = mContentResolver.insert(videoTable, mCurrentVideoValues); 908 Log.v(TAG, "Current video URI: " + mCurrentVideoUri); 909 } 910 mCurrentVideoValues = null; 911 } 912 913 private void deleteCurrentVideo() { 914 if (mCurrentVideoFilename != null) { 915 deleteVideoFile(mCurrentVideoFilename); 916 mCurrentVideoFilename = null; 917 } 918 if (mCurrentVideoUri != null) { 919 mContentResolver.delete(mCurrentVideoUri, null, null); 920 mCurrentVideoUri = null; 921 } 922 updateAndShowStorageHint(true); 923 } 924 925 private void deleteVideoFile(String fileName) { 926 Log.v(TAG, "Deleting video " + fileName); 927 File f = new File(fileName); 928 if (!f.delete()) { 929 Log.v(TAG, "Could not delete " + fileName); 930 } 931 } 932 933 private void addBaseMenuItems(Menu menu) { 934 MenuHelper.addSwitchModeMenuItem(menu, this, false); 935 { 936 MenuItem gallery = 937 menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0, 938 R.string.camera_gallery_photos_text).setOnMenuItemClickListener( 939 new OnMenuItemClickListener() { 940 public boolean onMenuItemClick(MenuItem item) { 941 gotoGallery(); 942 return true; 943 } 944 }); 945 gallery.setIcon(android.R.drawable.ic_menu_gallery); 946 mGalleryItems.add(gallery); 947 } 948 { 949 MenuItem gallery = 950 menu.add(MenuHelper.VIDEO_MODE_ITEM, MENU_GALLERY_VIDEOS, 0, 951 R.string.camera_gallery_photos_text).setOnMenuItemClickListener( 952 new OnMenuItemClickListener() { 953 public boolean onMenuItemClick(MenuItem item) { 954 gotoGallery(); 955 return true; 956 } 957 }); 958 gallery.setIcon(android.R.drawable.ic_menu_gallery); 959 mGalleryItems.add(gallery); 960 } 961 962 MenuItem item = 963 menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings) 964 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 965 public boolean onMenuItemClick(MenuItem item) { 966 // Keep the camera instance for a while. 967 // This avoids re-opening the camera and saves 968 // time. 969 CameraHolder.instance().keep(); 970 971 Intent intent = new Intent(); 972 intent.setClass(VideoCamera.this, CameraSettings.class); 973 startActivity(intent); 974 return true; 975 } 976 }); 977 item.setIcon(android.R.drawable.ic_menu_preferences); 978 } 979 980 // from MediaRecorder.OnErrorListener 981 public void onError(MediaRecorder mr, int what, int extra) { 982 if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { 983 // We may have run out of space on the sdcard. 984 stopVideoRecording(); 985 updateAndShowStorageHint(true); 986 } 987 } 988 989 // from MediaRecorder.OnInfoListener 990 public void onInfo(MediaRecorder mr, int what, int extra) { 991 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { 992 mShutterButton.performClick(); 993 } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 994 mShutterButton.performClick(); 995 updateAndShowStorageHint(true); 996 } 997 } 998 999 /* 1000 * Make sure we're not recording music playing in the background, ask the 1001 * MediaPlaybackService to pause playback. 1002 */ 1003 private void pauseAudioPlayback() { 1004 // Shamelessly copied from MediaPlaybackService.java, which 1005 // should be public, but isn't. 1006 Intent i = new Intent("com.android.music.musicservicecommand"); 1007 i.putExtra("command", "pause"); 1008 1009 sendBroadcast(i); 1010 } 1011 1012 private void startVideoRecording() { 1013 Log.v(TAG, "startVideoRecording"); 1014 if (!mMediaRecorderRecording) { 1015 1016 if (mStorageStatus != STORAGE_STATUS_OK) { 1017 Log.v(TAG, "Storage issue, ignore the start request"); 1018 return; 1019 } 1020 1021 // Check mMediaRecorder to see whether it is initialized or not. 1022 if (mMediaRecorder == null) { 1023 Log.e(TAG, "MediaRecorder is not initialized."); 1024 return; 1025 } 1026 1027 pauseAudioPlayback(); 1028 1029 try { 1030 mMediaRecorder.setOnErrorListener(this); 1031 mMediaRecorder.setOnInfoListener(this); 1032 mMediaRecorder.start(); // Recording is now started 1033 } catch (RuntimeException e) { 1034 Log.e(TAG, "Could not start media recorder. ", e); 1035 return; 1036 } 1037 mMediaRecorderRecording = true; 1038 mRecordingStartTime = SystemClock.uptimeMillis(); 1039 updateRecordingIndicator(false); 1040 mRecordingTimeView.setText(""); 1041 mRecordingTimeView.setVisibility(View.VISIBLE); 1042 mHandler.sendEmptyMessage(UPDATE_RECORD_TIME); 1043 setScreenTimeoutInfinite(); 1044 } 1045 } 1046 1047 private void updateRecordingIndicator(boolean showRecording) { 1048 int drawableId = 1049 showRecording ? R.drawable.btn_ic_video_record 1050 : R.drawable.btn_ic_video_record_stop; 1051 Drawable drawable = getResources().getDrawable(drawableId); 1052 mShutterButton.setImageDrawable(drawable); 1053 } 1054 1055 private void stopVideoRecordingAndGetThumbnail() { 1056 stopVideoRecording(); 1057 acquireVideoThumb(); 1058 } 1059 1060 private void stopVideoRecordingAndShowAlert() { 1061 stopVideoRecording(); 1062 showAlert(); 1063 } 1064 1065 private void showAlert() { 1066 mVideoPreview.setVisibility(View.INVISIBLE); 1067 fadeOut(findViewById(R.id.shutter_button)); 1068 if (mCurrentVideoFilename != null) { 1069 mVideoFrame.setImageBitmap( 1070 Util.createVideoThumbnail(mCurrentVideoFilename)); 1071 mVideoFrame.setVisibility(View.VISIBLE); 1072 } 1073 int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play}; 1074 for (int id : pickIds) { 1075 View button = findViewById(id); 1076 fadeIn(((View) button.getParent())); 1077 } 1078 } 1079 1080 private void hideAlert() { 1081 mVideoPreview.setVisibility(View.VISIBLE); 1082 mVideoFrame.setVisibility(View.INVISIBLE); 1083 fadeIn(findViewById(R.id.shutter_button)); 1084 int[] pickIds = {R.id.btn_retake, R.id.btn_done, R.id.btn_play}; 1085 for (int id : pickIds) { 1086 View button = findViewById(id); 1087 fadeOut(((View) button.getParent())); 1088 } 1089 } 1090 1091 private static void fadeIn(View view) { 1092 view.setVisibility(View.VISIBLE); 1093 Animation animation = new AlphaAnimation(0F, 1F); 1094 animation.setDuration(500); 1095 view.startAnimation(animation); 1096 } 1097 1098 private static void fadeOut(View view) { 1099 view.setVisibility(View.INVISIBLE); 1100 Animation animation = new AlphaAnimation(1F, 0F); 1101 animation.setDuration(500); 1102 view.startAnimation(animation); 1103 } 1104 1105 private boolean isAlertVisible() { 1106 return this.mVideoFrame.getVisibility() == View.VISIBLE; 1107 } 1108 1109 private void stopVideoRecordingAndShowReview() { 1110 stopVideoRecording(); 1111 if (mThumbController.isUriValid()) { 1112 Uri targetUri = mThumbController.getUri(); 1113 Intent intent = new Intent(this, ReviewImage.class); 1114 intent.setData(targetUri); 1115 intent.putExtra(MediaStore.EXTRA_FULL_SCREEN, true); 1116 intent.putExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, true); 1117 intent.putExtra("com.android.camera.ReviewMode", true); 1118 try { 1119 startActivity(intent); 1120 } catch (ActivityNotFoundException ex) { 1121 Log.e(TAG, "review video fail", ex); 1122 } 1123 } else { 1124 Log.e(TAG, "Can't view last video."); 1125 } 1126 } 1127 1128 private void stopVideoRecording() { 1129 Log.v(TAG, "stopVideoRecording"); 1130 boolean needToRegisterRecording = false; 1131 if (mMediaRecorderRecording || mMediaRecorder != null) { 1132 if (mMediaRecorderRecording && mMediaRecorder != null) { 1133 try { 1134 mMediaRecorder.setOnErrorListener(null); 1135 mMediaRecorder.setOnInfoListener(null); 1136 mMediaRecorder.stop(); 1137 } catch (RuntimeException e) { 1138 Log.e(TAG, "stop fail: " + e.getMessage()); 1139 } 1140 1141 mCurrentVideoFilename = mCameraVideoFilename; 1142 Log.v(TAG, "Setting current video filename: " + mCurrentVideoFilename); 1143 needToRegisterRecording = true; 1144 mMediaRecorderRecording = false; 1145 } 1146 releaseMediaRecorder(); 1147 updateRecordingIndicator(true); 1148 mRecordingTimeView.setVisibility(View.GONE); 1149 setScreenTimeoutLong(); 1150 } 1151 if (needToRegisterRecording && mStorageStatus == STORAGE_STATUS_OK) { 1152 registerVideo(); 1153 } 1154 1155 mCameraVideoFilename = null; 1156 mCameraVideoFileDescriptor = null; 1157 } 1158 1159 private void setScreenTimeoutSystemDefault() { 1160 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1161 clearScreenOnFlag(); 1162 } 1163 1164 private void setScreenTimeoutLong() { 1165 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1166 setScreenOnFlag(); 1167 mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1168 } 1169 1170 private void setScreenTimeoutInfinite() { 1171 mHandler.removeMessages(CLEAR_SCREEN_DELAY); 1172 setScreenOnFlag(); 1173 } 1174 1175 private void clearScreenOnFlag() { 1176 Window w = getWindow(); 1177 final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; 1178 if ((w.getAttributes().flags & flag) != 0) { 1179 w.clearFlags(flag); 1180 } 1181 } 1182 1183 private void setScreenOnFlag() { 1184 Window w = getWindow(); 1185 final int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; 1186 if ((w.getAttributes().flags & flag) == 0) { 1187 w.addFlags(flag); 1188 } 1189 } 1190 1191 private void hideAlertAndInitializeRecorder() { 1192 hideAlert(); 1193 mRecorderInitialized = false; 1194 mHandler.sendEmptyMessage(INIT_RECORDER); 1195 } 1196 1197 private void acquireVideoThumb() { 1198 Bitmap videoFrame = Util.createVideoThumbnail(mCurrentVideoFilename); 1199 mThumbController.setData(mCurrentVideoUri, videoFrame); 1200 } 1201 1202 private static ImageManager.DataLocation dataLocation() { 1203 return ImageManager.DataLocation.EXTERNAL; 1204 } 1205 1206 private void updateLastVideo() { 1207 IImageList list = 1208 ImageManager.allImages(mContentResolver, dataLocation(), 1209 ImageManager.INCLUDE_VIDEOS, ImageManager.SORT_ASCENDING, 1210 ImageManager.CAMERA_IMAGE_BUCKET_ID); 1211 int count = list.getCount(); 1212 if (count > 0) { 1213 IImage image = list.getImageAt(count - 1); 1214 Uri uri = image.fullSizeImageUri(); 1215 mThumbController.setData(uri, image.miniThumbBitmap()); 1216 } else { 1217 mThumbController.setData(null, null); 1218 } 1219 list.deactivate(); 1220 } 1221 1222 private void updateRecordingTime() { 1223 if (!mMediaRecorderRecording) { 1224 return; 1225 } 1226 long now = SystemClock.uptimeMillis(); 1227 long delta = now - mRecordingStartTime; 1228 1229 // Starting a minute before reaching the max duration 1230 // limit, we'll countdown the remaining time instead. 1231 boolean countdownRemainingTime = (delta >= mMaxVideoDurationInMs - 60000); 1232 1233 if (countdownRemainingTime) { 1234 delta = Math.max(0, mMaxVideoDurationInMs - delta); 1235 } 1236 1237 long seconds = (delta + 500) / 1000; // round to nearest 1238 long minutes = seconds / 60; 1239 long hours = minutes / 60; 1240 long remainderMinutes = minutes - (hours * 60); 1241 long remainderSeconds = seconds - (minutes * 60); 1242 1243 String secondsString = Long.toString(remainderSeconds); 1244 if (secondsString.length() < 2) { 1245 secondsString = "0" + secondsString; 1246 } 1247 String minutesString = Long.toString(remainderMinutes); 1248 if (minutesString.length() < 2) { 1249 minutesString = "0" + minutesString; 1250 } 1251 String text = minutesString + ":" + secondsString; 1252 if (hours > 0) { 1253 String hoursString = Long.toString(hours); 1254 if (hoursString.length() < 2) { 1255 hoursString = "0" + hoursString; 1256 } 1257 text = hoursString + ":" + text; 1258 } 1259 mRecordingTimeView.setText(text); 1260 1261 if (mRecordingTimeCountsDown != countdownRemainingTime) { 1262 // Avoid setting the color on every update, do it only 1263 // when it needs changing. 1264 1265 mRecordingTimeCountsDown = countdownRemainingTime; 1266 1267 int color = 1268 getResources().getColor( 1269 countdownRemainingTime ? R.color.recording_time_remaining_text 1270 : R.color.recording_time_elapsed_text); 1271 1272 mRecordingTimeView.setTextColor(color); 1273 } 1274 1275 // Work around a limitation of the T-Mobile G1: The T-Mobile 1276 // hardware blitter can't pixel-accurately scale and clip at the 1277 // same time, and the SurfaceFlinger doesn't attempt to work around 1278 // this limitation. In order to avoid visual corruption we must 1279 // manually refresh the entire surface view when changing any 1280 // overlapping view's contents. 1281 mVideoPreview.invalidate(); 1282 mHandler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000); 1283 } 1284} 1285