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