1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.testingcamera; 18 19 import android.Manifest; 20 import android.annotation.SuppressLint; 21 import android.app.Activity; 22 import android.app.FragmentManager; 23 import android.content.pm.PackageManager; 24 import android.content.res.Resources; 25 import android.graphics.ImageFormat; 26 import android.hardware.Camera; 27 import android.hardware.Camera.Parameters; 28 import android.hardware.Camera.ErrorCallback; 29 import android.media.CamcorderProfile; 30 import android.media.MediaRecorder; 31 import android.media.MediaScannerConnection; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Environment; 35 import android.os.Handler; 36 import android.os.SystemClock; 37 import android.view.OrientationEventListener; 38 import android.view.View; 39 import android.view.Surface; 40 import android.view.SurfaceHolder; 41 import android.view.SurfaceView; 42 import android.view.View.OnClickListener; 43 import android.widget.AdapterView; 44 import android.widget.AdapterView.OnItemSelectedListener; 45 import android.widget.ArrayAdapter; 46 import android.widget.Button; 47 import android.widget.CheckBox; 48 import android.widget.LinearLayout; 49 import android.widget.LinearLayout.LayoutParams; 50 import android.widget.SeekBar; 51 import android.widget.Spinner; 52 import android.widget.TextView; 53 import android.widget.ToggleButton; 54 import android.renderscript.RenderScript; 55 import android.text.Layout; 56 import android.text.method.ScrollingMovementMethod; 57 import android.util.Log; 58 import android.util.SparseArray; 59 60 import java.io.File; 61 import java.io.IOException; 62 import java.io.PrintWriter; 63 import java.io.StringWriter; 64 import java.text.SimpleDateFormat; 65 import java.util.ArrayList; 66 import java.util.Date; 67 import java.util.HashSet; 68 import java.util.List; 69 import java.util.Set; 70 71 /** 72 * A simple test application for the camera API. 73 * 74 * The goal of this application is to allow all camera API features to be 75 * exercised, and all information provided by the API to be shown. 76 */ 77 public class TestingCamera extends Activity 78 implements SurfaceHolder.Callback, Camera.PreviewCallback, 79 Camera.ErrorCallback { 80 81 /** UI elements */ 82 private SurfaceView mPreviewView; 83 private SurfaceHolder mPreviewHolder; 84 private LinearLayout mPreviewColumn; 85 86 private SurfaceView mCallbackView; 87 private SurfaceHolder mCallbackHolder; 88 89 private Spinner mCameraSpinner; 90 private CheckBox mKeepOpenCheckBox; 91 private Button mInfoButton; 92 private Spinner mPreviewSizeSpinner; 93 private Spinner mPreviewFrameRateSpinner; 94 private ToggleButton mPreviewToggle; 95 private ToggleButton mHDRToggle; 96 private Spinner mAutofocusModeSpinner; 97 private Button mAutofocusButton; 98 private Button mCancelAutofocusButton; 99 private TextView mFlashModeSpinnerLabel; 100 private Spinner mFlashModeSpinner; 101 private ToggleButton mExposureLockToggle; 102 private Spinner mSnapshotSizeSpinner; 103 private Button mTakePictureButton; 104 private Spinner mCamcorderProfileSpinner; 105 private Spinner mVideoRecordSizeSpinner; 106 private Spinner mVideoFrameRateSpinner; 107 private ToggleButton mRecordToggle; 108 private CheckBox mRecordHandoffCheckBox; 109 private ToggleButton mRecordStabilizationToggle; 110 private ToggleButton mRecordHintToggle; 111 private ToggleButton mLockCameraToggle; 112 private Spinner mCallbackFormatSpinner; 113 private ToggleButton mCallbackToggle; 114 private TextView mColorEffectSpinnerLabel; 115 private Spinner mColorEffectSpinner; 116 private SeekBar mZoomSeekBar; 117 118 private TextView mLogView; 119 120 SnapshotDialogFragment mSnapshotDialog = null; 121 122 private Set<View> mOpenOnlyControls = new HashSet<View>(); 123 private Set<View> mPreviewOnlyControls = new HashSet<View>(); 124 125 private SparseArray<String> mFormatNames; 126 127 /** Camera state */ 128 private int mCameraId; 129 private Camera mCamera; 130 private Camera.Parameters mParams; 131 private List<Camera.Size> mPreviewSizes; 132 private int mPreviewSize = 0; 133 private List<Integer> mPreviewFrameRates; 134 private int mPreviewFrameRate = 0; 135 private List<Integer> mPreviewFormats; 136 private int mPreviewFormat = 0; 137 private List<String> mAfModes; 138 private int mAfMode = 0; 139 private List<String> mFlashModes; 140 private int mFlashMode = 0; 141 private List<Camera.Size> mSnapshotSizes; 142 private int mSnapshotSize = 0; 143 private List<CamcorderProfile> mCamcorderProfiles; 144 private int mCamcorderProfile = 0; 145 private List<Camera.Size> mVideoRecordSizes; 146 private int mVideoRecordSize = 0; 147 private List<Integer> mVideoFrameRates; 148 private int mVideoFrameRate = 0; 149 private List<String> mColorEffects; 150 private int mColorEffect = 0; 151 private int mZoom = 0; 152 153 private MediaRecorder mRecorder; 154 private File mRecordingFile; 155 156 private RenderScript mRS; 157 158 private boolean mCallbacksEnabled = false; 159 private CallbackProcessor mCallbackProcessor = null; 160 long mLastCallbackTimestamp = -1; 161 float mCallbackAvgFrameDuration = 30; 162 int mCallbackFrameCount = 0; 163 private static final float MEAN_FPS_HISTORY_COEFF = 0.9f; 164 private static final float MEAN_FPS_MEASUREMENT_COEFF = 0.1f; 165 private static final int FPS_REPORTING_PERIOD = 200; // frames 166 private static final int CALLBACK_BUFFER_COUNT = 3; 167 168 private static final int CAMERA_UNINITIALIZED = 0; 169 private static final int CAMERA_OPEN = 1; 170 private static final int CAMERA_PREVIEW = 2; 171 private static final int CAMERA_TAKE_PICTURE = 3; 172 private static final int CAMERA_RECORD = 4; 173 private int mState = CAMERA_UNINITIALIZED; 174 175 private static final int NO_CAMERA_ID = -1; 176 177 /** Misc variables */ 178 179 private static final String TAG = "TestingCamera"; 180 private static final int PERMISSIONS_REQUEST_CAMERA = 1; 181 private static final int PERMISSIONS_REQUEST_RECORDING = 2; 182 static final int PERMISSIONS_REQUEST_SNAPSHOT = 3; 183 private OrientationEventHandler mOrientationHandler; 184 185 /** Activity lifecycle */ 186 187 @Override onCreate(Bundle savedInstanceState)188 public void onCreate(Bundle savedInstanceState) { 189 super.onCreate(savedInstanceState); 190 191 setContentView(R.layout.main); 192 193 mPreviewColumn = (LinearLayout) findViewById(R.id.preview_column); 194 195 mPreviewView = (SurfaceView) findViewById(R.id.preview); 196 mPreviewView.getHolder().addCallback(this); 197 198 mCallbackView = (SurfaceView)findViewById(R.id.callback_view); 199 200 mCameraSpinner = (Spinner) findViewById(R.id.camera_spinner); 201 mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener); 202 203 mKeepOpenCheckBox = (CheckBox) findViewById(R.id.keep_open_checkbox); 204 205 mInfoButton = (Button) findViewById(R.id.info_button); 206 mInfoButton.setOnClickListener(mInfoButtonListener); 207 mOpenOnlyControls.add(mInfoButton); 208 209 mPreviewSizeSpinner = (Spinner) findViewById(R.id.preview_size_spinner); 210 mPreviewSizeSpinner.setOnItemSelectedListener(mPreviewSizeListener); 211 mOpenOnlyControls.add(mPreviewSizeSpinner); 212 213 mPreviewFrameRateSpinner = (Spinner) findViewById(R.id.preview_frame_rate_spinner); 214 mPreviewFrameRateSpinner.setOnItemSelectedListener(mPreviewFrameRateListener); 215 mOpenOnlyControls.add(mPreviewFrameRateSpinner); 216 217 mHDRToggle = (ToggleButton) findViewById(R.id.hdr_mode); 218 mHDRToggle.setOnClickListener(mHDRToggleListener); 219 mOpenOnlyControls.add(mHDRToggle); 220 221 mPreviewToggle = (ToggleButton) findViewById(R.id.start_preview); 222 mPreviewToggle.setOnClickListener(mPreviewToggleListener); 223 mOpenOnlyControls.add(mPreviewToggle); 224 225 mAutofocusModeSpinner = (Spinner) findViewById(R.id.af_mode_spinner); 226 mAutofocusModeSpinner.setOnItemSelectedListener(mAutofocusModeListener); 227 mOpenOnlyControls.add(mAutofocusModeSpinner); 228 229 mAutofocusButton = (Button) findViewById(R.id.af_button); 230 mAutofocusButton.setOnClickListener(mAutofocusButtonListener); 231 mPreviewOnlyControls.add(mAutofocusButton); 232 233 mCancelAutofocusButton = (Button) findViewById(R.id.af_cancel_button); 234 mCancelAutofocusButton.setOnClickListener(mCancelAutofocusButtonListener); 235 mPreviewOnlyControls.add(mCancelAutofocusButton); 236 237 mFlashModeSpinnerLabel = (TextView) findViewById(R.id.flash_mode_spinner_label); 238 239 mFlashModeSpinner = (Spinner) findViewById(R.id.flash_mode_spinner); 240 mFlashModeSpinner.setOnItemSelectedListener(mFlashModeListener); 241 mOpenOnlyControls.add(mFlashModeSpinner); 242 243 mExposureLockToggle = (ToggleButton) findViewById(R.id.exposure_lock); 244 mExposureLockToggle.setOnClickListener(mExposureLockToggleListener); 245 mOpenOnlyControls.add(mExposureLockToggle); 246 247 mZoomSeekBar = (SeekBar) findViewById(R.id.zoom_seekbar); 248 mZoomSeekBar.setOnSeekBarChangeListener(mZoomSeekBarListener); 249 250 mSnapshotSizeSpinner = (Spinner) findViewById(R.id.snapshot_size_spinner); 251 mSnapshotSizeSpinner.setOnItemSelectedListener(mSnapshotSizeListener); 252 mOpenOnlyControls.add(mSnapshotSizeSpinner); 253 254 mTakePictureButton = (Button) findViewById(R.id.take_picture); 255 mTakePictureButton.setOnClickListener(mTakePictureListener); 256 mPreviewOnlyControls.add(mTakePictureButton); 257 258 mCamcorderProfileSpinner = (Spinner) findViewById(R.id.camcorder_profile_spinner); 259 mCamcorderProfileSpinner.setOnItemSelectedListener(mCamcorderProfileListener); 260 mOpenOnlyControls.add(mCamcorderProfileSpinner); 261 262 mVideoRecordSizeSpinner = (Spinner) findViewById(R.id.video_record_size_spinner); 263 mVideoRecordSizeSpinner.setOnItemSelectedListener(mVideoRecordSizeListener); 264 mOpenOnlyControls.add(mVideoRecordSizeSpinner); 265 266 mVideoFrameRateSpinner = (Spinner) findViewById(R.id.video_frame_rate_spinner); 267 mVideoFrameRateSpinner.setOnItemSelectedListener(mVideoFrameRateListener); 268 mOpenOnlyControls.add(mVideoFrameRateSpinner); 269 270 mRecordToggle = (ToggleButton) findViewById(R.id.start_record); 271 mRecordToggle.setOnClickListener(mRecordToggleListener); 272 mPreviewOnlyControls.add(mRecordToggle); 273 274 mRecordHandoffCheckBox = (CheckBox) findViewById(R.id.record_handoff_checkbox); 275 276 mRecordStabilizationToggle = (ToggleButton) findViewById(R.id.record_stabilization); 277 mRecordStabilizationToggle.setOnClickListener(mRecordStabilizationToggleListener); 278 mOpenOnlyControls.add(mRecordStabilizationToggle); 279 280 mRecordHintToggle = (ToggleButton) findViewById(R.id.record_hint); 281 mRecordHintToggle.setOnClickListener(mRecordHintToggleListener); 282 mOpenOnlyControls.add(mRecordHintToggle); 283 284 mLockCameraToggle = (ToggleButton) findViewById(R.id.lock_camera); 285 mLockCameraToggle.setOnClickListener(mLockCameraToggleListener); 286 mLockCameraToggle.setChecked(true); // ON by default 287 mOpenOnlyControls.add(mLockCameraToggle); 288 289 mCallbackFormatSpinner = (Spinner) findViewById(R.id.callback_format_spinner); 290 mCallbackFormatSpinner.setOnItemSelectedListener(mCallbackFormatListener); 291 mOpenOnlyControls.add(mCallbackFormatSpinner); 292 293 mCallbackToggle = (ToggleButton) findViewById(R.id.enable_callbacks); 294 mCallbackToggle.setOnClickListener(mCallbackToggleListener); 295 mOpenOnlyControls.add(mCallbackToggle); 296 297 mColorEffectSpinnerLabel = (TextView) findViewById(R.id.color_effect_spinner_label); 298 299 mColorEffectSpinner = (Spinner) findViewById(R.id.color_effect_spinner); 300 mColorEffectSpinner.setOnItemSelectedListener(mColorEffectListener); 301 mOpenOnlyControls.add(mColorEffectSpinner); 302 303 mLogView = (TextView) findViewById(R.id.log); 304 mLogView.setMovementMethod(new ScrollingMovementMethod()); 305 306 mOpenOnlyControls.addAll(mPreviewOnlyControls); 307 308 mFormatNames = new SparseArray<String>(7); 309 mFormatNames.append(ImageFormat.JPEG, "JPEG"); 310 mFormatNames.append(ImageFormat.NV16, "NV16"); 311 mFormatNames.append(ImageFormat.NV21, "NV21"); 312 mFormatNames.append(ImageFormat.RGB_565, "RGB_565"); 313 mFormatNames.append(ImageFormat.UNKNOWN, "UNKNOWN"); 314 mFormatNames.append(ImageFormat.YUY2, "YUY2"); 315 mFormatNames.append(ImageFormat.YV12, "YV12"); 316 317 int numCameras = Camera.getNumberOfCameras(); 318 String[] cameraNames = new String[numCameras + 1]; 319 cameraNames[0] = "None"; 320 for (int i = 0; i < numCameras; i++) { 321 cameraNames[i + 1] = "Camera " + i; 322 } 323 324 mCameraSpinner.setAdapter( 325 new ArrayAdapter<String>(this, 326 R.layout.spinner_item, cameraNames)); 327 if (numCameras > 0) { 328 mCameraId = 0; 329 mCameraSpinner.setSelection(mCameraId + 1); 330 } else { 331 resetCamera(); 332 mCameraSpinner.setSelection(0); 333 } 334 335 mRS = RenderScript.create(this); 336 337 mOrientationHandler = new OrientationEventHandler(this); 338 } 339 340 private static class OrientationEventHandler extends OrientationEventListener { 341 private TestingCamera mActivity; 342 private int mCurrentRotation = -1; OrientationEventHandler(TestingCamera activity)343 OrientationEventHandler(TestingCamera activity) { 344 super(activity); 345 mActivity = activity; 346 } 347 348 @Override onOrientationChanged(int orientation)349 public void onOrientationChanged(int orientation) { 350 if (mActivity != null) { 351 int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); 352 if (mCurrentRotation != rotation) { 353 mCurrentRotation = rotation; 354 mActivity.runOnUiThread(new Runnable() { 355 @Override 356 public void run() { 357 mActivity.setCameraDisplayOrientation(); 358 mActivity.resizePreview(); 359 } 360 }); 361 } 362 } 363 } 364 } 365 366 @Override onResume()367 public void onResume() { 368 super.onResume(); 369 log("onResume: Setting up"); 370 setUpCamera(); 371 mOrientationHandler.enable(); 372 } 373 374 @Override onPause()375 public void onPause() { 376 super.onPause(); 377 if (mState == CAMERA_RECORD) { 378 stopRecording(false); 379 } 380 if (mKeepOpenCheckBox.isChecked()) { 381 log("onPause: Not releasing camera"); 382 383 if (mState == CAMERA_PREVIEW) { 384 mCamera.stopPreview(); 385 mState = CAMERA_OPEN; 386 } 387 } else { 388 log("onPause: Releasing camera"); 389 390 if (mCamera != null) { 391 mCamera.release(); 392 } 393 mState = CAMERA_UNINITIALIZED; 394 } 395 mOrientationHandler.disable(); 396 } 397 398 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)399 public void onRequestPermissionsResult (int requestCode, String[] permissions, 400 int[] grantResults) { 401 if (requestCode == PERMISSIONS_REQUEST_CAMERA) { 402 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 403 log("Camera permission granted"); 404 setUpCamera(); 405 } else { 406 log("Camera permission denied, can't do anything"); 407 finish(); 408 } 409 } else if (requestCode == PERMISSIONS_REQUEST_RECORDING) { 410 mRecordToggle.setChecked(false); 411 for (int i = 0; i < grantResults.length; i++) { 412 if (grantResults[i] == PackageManager.PERMISSION_DENIED) { 413 log("Recording permission " + permissions[i] + " denied"); 414 return; 415 } 416 log("Recording permissions granted"); 417 setUpCamera(); 418 } 419 } else if (requestCode == PERMISSIONS_REQUEST_SNAPSHOT) { 420 if (mSnapshotDialog != null) { 421 mSnapshotDialog.onRequestPermissionsResult(requestCode, permissions, 422 grantResults); 423 } 424 } 425 426 } 427 428 /** SurfaceHolder.Callback methods */ 429 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)430 public void surfaceChanged(SurfaceHolder holder, 431 int format, 432 int width, 433 int height) { 434 if (holder == mPreviewView.getHolder()) { 435 if (mState >= CAMERA_OPEN) { 436 final int previewWidth = 437 mPreviewSizes.get(mPreviewSize).width; 438 final int previewHeight = 439 mPreviewSizes.get(mPreviewSize).height; 440 441 if ( Math.abs((float)previewWidth / previewHeight - 442 (float)width/height) > 0.01f) { 443 Handler h = new Handler(); 444 h.post(new Runnable() { 445 @Override 446 public void run() { 447 layoutPreview(); 448 } 449 }); 450 } 451 } 452 453 if (mPreviewHolder != null || mState == CAMERA_UNINITIALIZED) { 454 return; 455 } 456 log("Surface holder available: " + width + " x " + height); 457 mPreviewHolder = holder; 458 try { 459 if (mCamera != null) { 460 mCamera.setPreviewDisplay(holder); 461 } 462 } catch (IOException e) { 463 logE("Unable to set up preview!"); 464 } 465 } else if (holder == mCallbackView.getHolder()) { 466 mCallbackHolder = holder; 467 } 468 } 469 470 @Override surfaceCreated(SurfaceHolder holder)471 public void surfaceCreated(SurfaceHolder holder) { 472 473 } 474 475 @Override surfaceDestroyed(SurfaceHolder holder)476 public void surfaceDestroyed(SurfaceHolder holder) { 477 mPreviewHolder = null; 478 } 479 setCameraDisplayOrientation()480 public void setCameraDisplayOrientation() { 481 android.hardware.Camera.CameraInfo info = 482 new android.hardware.Camera.CameraInfo(); 483 android.hardware.Camera.getCameraInfo(mCameraId, info); 484 int rotation = getWindowManager().getDefaultDisplay() 485 .getRotation(); 486 int degrees = 0; 487 switch (rotation) { 488 case Surface.ROTATION_0: degrees = 0; break; 489 case Surface.ROTATION_90: degrees = 90; break; 490 case Surface.ROTATION_180: degrees = 180; break; 491 case Surface.ROTATION_270: degrees = 270; break; 492 } 493 494 int result; 495 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 496 result = (info.orientation + degrees) % 360; 497 result = (360 - result) % 360; // compensate the mirror 498 } else { // back-facing 499 result = (info.orientation - degrees + 360) % 360; 500 } 501 log(String.format( 502 "Camera sensor orientation %d, UI rotation %d, facing %s. Final orientation %d", 503 info.orientation, rotation, 504 info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT ? "FRONT" : "BACK", 505 result)); 506 mCamera.setDisplayOrientation(result); 507 } 508 509 /** UI controls enable/disable for all open-only controls */ enableOpenOnlyControls(boolean enabled)510 private void enableOpenOnlyControls(boolean enabled) { 511 for (View v : mOpenOnlyControls) { 512 v.setEnabled(enabled); 513 } 514 } 515 516 /** UI controls enable/disable for all preview-only controls */ enablePreviewOnlyControls(boolean enabled)517 private void enablePreviewOnlyControls(boolean enabled) { 518 for (View v : mPreviewOnlyControls) { 519 v.setEnabled(enabled); 520 } 521 } 522 523 /** UI listeners */ 524 525 private AdapterView.OnItemSelectedListener mCameraSpinnerListener = 526 new AdapterView.OnItemSelectedListener() { 527 @Override 528 public void onItemSelected(AdapterView<?> parent, 529 View view, int pos, long id) { 530 int cameraId = pos - 1; 531 if (mCameraId != cameraId) { 532 resetCamera(); 533 mCameraId = cameraId; 534 mPreviewToggle.setChecked(false); 535 setUpCamera(); 536 } 537 } 538 539 @Override 540 public void onNothingSelected(AdapterView<?> parent) { 541 542 } 543 }; 544 545 private OnClickListener mInfoButtonListener = new OnClickListener() { 546 @Override 547 public void onClick(View v) { 548 if (mCameraId != NO_CAMERA_ID) { 549 FragmentManager fm = getFragmentManager(); 550 InfoDialogFragment infoDialog = new InfoDialogFragment(); 551 infoDialog.updateInfo(mCameraId, mCamera); 552 infoDialog.show(fm, "info_dialog_fragment"); 553 } 554 } 555 }; 556 557 private AdapterView.OnItemSelectedListener mPreviewSizeListener = 558 new AdapterView.OnItemSelectedListener() { 559 @Override 560 public void onItemSelected(AdapterView<?> parent, 561 View view, int pos, long id) { 562 if (pos == mPreviewSize) return; 563 if (mState == CAMERA_PREVIEW) { 564 log("Stopping preview and callbacks to switch resolutions"); 565 stopCallbacks(); 566 mCamera.stopPreview(); 567 } 568 569 mPreviewSize = pos; 570 int width = mPreviewSizes.get(mPreviewSize).width; 571 int height = mPreviewSizes.get(mPreviewSize).height; 572 mParams.setPreviewSize(width, height); 573 574 log("Setting preview size to " + width + "x" + height); 575 576 mCamera.setParameters(mParams); 577 resizePreview(); 578 579 if (mState == CAMERA_PREVIEW) { 580 log("Restarting preview"); 581 mCamera.startPreview(); 582 } 583 } 584 585 @Override 586 public void onNothingSelected(AdapterView<?> parent) { 587 588 } 589 }; 590 591 private AdapterView.OnItemSelectedListener mPreviewFrameRateListener = 592 new AdapterView.OnItemSelectedListener() { 593 @Override 594 public void onItemSelected(AdapterView<?> parent, 595 View view, int pos, long id) { 596 if (pos == mPreviewFrameRate) return; 597 mPreviewFrameRate = pos; 598 mParams.setPreviewFrameRate(mPreviewFrameRates.get(mPreviewFrameRate)); 599 600 log("Setting preview frame rate to " + ((TextView)view).getText()); 601 602 mCamera.setParameters(mParams); 603 } 604 605 @Override 606 public void onNothingSelected(AdapterView<?> parent) { 607 608 } 609 }; 610 611 private View.OnClickListener mHDRToggleListener = 612 new View.OnClickListener() { 613 @Override 614 public void onClick(View v) { 615 if (mState == CAMERA_TAKE_PICTURE) { 616 logE("Can't change preview state while taking picture!"); 617 return; 618 } 619 620 if (mHDRToggle.isChecked()) { 621 log("Turning on HDR"); 622 mParams.setSceneMode(Camera.Parameters.SCENE_MODE_HDR); 623 } else { 624 log("Turning off HDR"); 625 mParams.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO); 626 } 627 mCamera.setParameters(mParams); 628 } 629 }; 630 631 private View.OnClickListener mPreviewToggleListener = 632 new View.OnClickListener() { 633 @Override 634 public void onClick(View v) { 635 if (mState == CAMERA_TAKE_PICTURE) { 636 logE("Can't change preview state while taking picture!"); 637 return; 638 } 639 if (mPreviewToggle.isChecked()) { 640 log("Starting preview"); 641 mCamera.startPreview(); 642 mState = CAMERA_PREVIEW; 643 enablePreviewOnlyControls(true); 644 } else { 645 log("Stopping preview"); 646 mCamera.stopPreview(); 647 mState = CAMERA_OPEN; 648 649 enablePreviewOnlyControls(false); 650 } 651 } 652 }; 653 654 private OnItemSelectedListener mAutofocusModeListener = 655 new OnItemSelectedListener() { 656 @Override 657 public void onItemSelected(AdapterView<?> parent, 658 View view, int pos, long id) { 659 if (pos == mAfMode) return; 660 661 mAfMode = pos; 662 String focusMode = mAfModes.get(mAfMode); 663 log("Setting focus mode to " + focusMode); 664 if (focusMode == Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE || 665 focusMode == Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) { 666 mCamera.setAutoFocusMoveCallback(mAutofocusMoveCallback); 667 } 668 mParams.setFocusMode(focusMode); 669 670 mCamera.setParameters(mParams); 671 } 672 673 @Override 674 public void onNothingSelected(AdapterView<?> arg0) { 675 676 } 677 }; 678 679 private OnClickListener mAutofocusButtonListener = 680 new View.OnClickListener() { 681 @Override 682 public void onClick(View v) { 683 log("Triggering autofocus"); 684 mCamera.autoFocus(mAutofocusCallback); 685 } 686 }; 687 688 private OnClickListener mCancelAutofocusButtonListener = 689 new View.OnClickListener() { 690 @Override 691 public void onClick(View v) { 692 log("Cancelling autofocus"); 693 mCamera.cancelAutoFocus(); 694 } 695 }; 696 697 private Camera.AutoFocusCallback mAutofocusCallback = 698 new Camera.AutoFocusCallback() { 699 @Override 700 public void onAutoFocus(boolean success, Camera camera) { 701 log("Autofocus completed: " + (success ? "success" : "failure") ); 702 } 703 }; 704 705 private Camera.AutoFocusMoveCallback mAutofocusMoveCallback = 706 new Camera.AutoFocusMoveCallback() { 707 @Override 708 public void onAutoFocusMoving(boolean start, Camera camera) { 709 log("Autofocus movement: " + (start ? "starting" : "stopped") ); 710 } 711 }; 712 713 private OnItemSelectedListener mFlashModeListener = 714 new OnItemSelectedListener() { 715 @Override 716 public void onItemSelected(AdapterView<?> parent, 717 View view, int pos, long id) { 718 if (pos == mFlashMode) return; 719 720 mFlashMode = pos; 721 String flashMode = mFlashModes.get(mFlashMode); 722 log("Setting flash mode to " + flashMode); 723 mParams.setFlashMode(flashMode); 724 mCamera.setParameters(mParams); 725 } 726 727 @Override 728 public void onNothingSelected(AdapterView<?> arg0) { 729 730 } 731 }; 732 733 734 private AdapterView.OnItemSelectedListener mSnapshotSizeListener = 735 new AdapterView.OnItemSelectedListener() { 736 @Override 737 public void onItemSelected(AdapterView<?> parent, 738 View view, int pos, long id) { 739 if (pos == mSnapshotSize) return; 740 741 mSnapshotSize = pos; 742 int width = mSnapshotSizes.get(mSnapshotSize).width; 743 int height = mSnapshotSizes.get(mSnapshotSize).height; 744 log("Setting snapshot size to " + width + " x " + height); 745 746 mParams.setPictureSize(width, height); 747 748 mCamera.setParameters(mParams); 749 } 750 751 @Override 752 public void onNothingSelected(AdapterView<?> parent) { 753 754 } 755 }; 756 757 private View.OnClickListener mTakePictureListener = 758 new View.OnClickListener() { 759 @Override 760 public void onClick(View v) { 761 log("Taking picture"); 762 if (mState == CAMERA_PREVIEW) { 763 mState = CAMERA_TAKE_PICTURE; 764 enablePreviewOnlyControls(false); 765 mPreviewToggle.setChecked(false); 766 767 mCamera.takePicture(mShutterCb, mRawCb, mPostviewCb, mJpegCb); 768 } else { 769 logE("Can't take picture while not running preview!"); 770 } 771 } 772 }; 773 774 private AdapterView.OnItemSelectedListener mCamcorderProfileListener = 775 new AdapterView.OnItemSelectedListener() { 776 @Override 777 public void onItemSelected(AdapterView<?> parent, 778 View view, int pos, long id) { 779 if (pos != mCamcorderProfile) { 780 log("Setting camcorder profile to " + ((TextView)view).getText()); 781 mCamcorderProfile = pos; 782 } 783 784 // Additionally change video recording size to match 785 mVideoRecordSize = 0; // "default", in case it's not found 786 int width = mCamcorderProfiles.get(pos).videoFrameWidth; 787 int height = mCamcorderProfiles.get(pos).videoFrameHeight; 788 for (int i = 0; i < mVideoRecordSizes.size(); i++) { 789 Camera.Size s = mVideoRecordSizes.get(i); 790 if (width == s.width && height == s.height) { 791 mVideoRecordSize = i; 792 break; 793 } 794 } 795 log("Setting video record size to " + mVideoRecordSize); 796 mVideoRecordSizeSpinner.setSelection(mVideoRecordSize); 797 } 798 799 @Override 800 public void onNothingSelected(AdapterView<?> parent) { 801 802 } 803 }; 804 805 private AdapterView.OnItemSelectedListener mVideoRecordSizeListener = 806 new AdapterView.OnItemSelectedListener() { 807 @Override 808 public void onItemSelected(AdapterView<?> parent, 809 View view, int pos, long id) { 810 if (pos == mVideoRecordSize) return; 811 812 log("Setting video record size to " + ((TextView)view).getText()); 813 mVideoRecordSize = pos; 814 } 815 816 @Override 817 public void onNothingSelected(AdapterView<?> parent) { 818 819 } 820 }; 821 822 private AdapterView.OnItemSelectedListener mVideoFrameRateListener = 823 new AdapterView.OnItemSelectedListener() { 824 @Override 825 public void onItemSelected(AdapterView<?> parent, 826 View view, int pos, long id) { 827 if (pos == mVideoFrameRate) return; 828 829 log("Setting video frame rate to " + ((TextView)view).getText()); 830 mVideoFrameRate = pos; 831 } 832 833 @Override 834 public void onNothingSelected(AdapterView<?> parent) { 835 836 } 837 }; 838 839 private View.OnClickListener mRecordToggleListener = 840 new View.OnClickListener() { 841 @Override 842 public void onClick(View v) { 843 if (!mLockCameraToggle.isChecked()) { 844 logE("Re-lock camera before recording"); 845 return; 846 } 847 848 mPreviewToggle.setEnabled(false); 849 if (mState == CAMERA_PREVIEW) { 850 startRecording(); 851 } else if (mState == CAMERA_RECORD) { 852 stopRecording(false); 853 } else { 854 logE("Can't toggle recording in current state!"); 855 } 856 mPreviewToggle.setEnabled(true); 857 } 858 }; 859 860 private View.OnClickListener mRecordStabilizationToggleListener = 861 new View.OnClickListener() { 862 @Override 863 public void onClick(View v) { 864 boolean on = ((ToggleButton) v).isChecked(); 865 mParams.setVideoStabilization(on); 866 867 mCamera.setParameters(mParams); 868 } 869 }; 870 871 private View.OnClickListener mRecordHintToggleListener = 872 new View.OnClickListener() { 873 @Override 874 public void onClick(View v) { 875 boolean on = ((ToggleButton) v).isChecked(); 876 mParams.setRecordingHint(on); 877 878 mCamera.setParameters(mParams); 879 } 880 }; 881 882 private View.OnClickListener mLockCameraToggleListener = 883 new View.OnClickListener() { 884 @Override 885 public void onClick(View v) { 886 887 if (mState == CAMERA_RECORD) { 888 logE("Stop recording before toggling lock"); 889 return; 890 } 891 892 boolean on = ((ToggleButton) v).isChecked(); 893 894 if (on) { 895 mCamera.lock(); 896 log("Locked camera"); 897 } else { 898 mCamera.unlock(); 899 log("Unlocked camera"); 900 } 901 } 902 }; 903 904 private Camera.ShutterCallback mShutterCb = new Camera.ShutterCallback() { 905 @Override 906 public void onShutter() { 907 log("Shutter callback received"); 908 } 909 }; 910 911 private Camera.PictureCallback mRawCb = new Camera.PictureCallback() { 912 @Override 913 public void onPictureTaken(byte[] data, Camera camera) { 914 log("Raw callback received"); 915 } 916 }; 917 918 private Camera.PictureCallback mPostviewCb = new Camera.PictureCallback() { 919 @Override 920 public void onPictureTaken(byte[] data, Camera camera) { 921 log("Postview callback received"); 922 } 923 }; 924 925 private Camera.PictureCallback mJpegCb = new Camera.PictureCallback() { 926 @Override 927 public void onPictureTaken(byte[] data, Camera camera) { 928 log("JPEG picture callback received"); 929 FragmentManager fm = getFragmentManager(); 930 mSnapshotDialog = new SnapshotDialogFragment(); 931 932 mSnapshotDialog.updateImage(data); 933 mSnapshotDialog.show(fm, "snapshot_dialog_fragment"); 934 935 mPreviewToggle.setEnabled(true); 936 937 mState = CAMERA_OPEN; 938 } 939 }; 940 941 private AdapterView.OnItemSelectedListener mCallbackFormatListener = 942 new AdapterView.OnItemSelectedListener() { 943 public void onItemSelected(AdapterView<?> parent, 944 View view, int pos, long id) { 945 mPreviewFormat = pos; 946 947 log("Setting preview format to " + 948 mFormatNames.get(mPreviewFormats.get(mPreviewFormat))); 949 950 switch (mState) { 951 case CAMERA_UNINITIALIZED: 952 return; 953 case CAMERA_OPEN: 954 break; 955 case CAMERA_PREVIEW: 956 if (mCallbacksEnabled) { 957 log("Stopping preview and callbacks to switch formats"); 958 stopCallbacks(); 959 mCamera.stopPreview(); 960 } 961 break; 962 case CAMERA_RECORD: 963 logE("Can't update format while recording active"); 964 return; 965 } 966 967 mParams.setPreviewFormat(mPreviewFormats.get(mPreviewFormat)); 968 mCamera.setParameters(mParams); 969 970 if (mCallbacksEnabled) { 971 if (mState == CAMERA_PREVIEW) { 972 mCamera.startPreview(); 973 } 974 } 975 976 configureCallbacks(mCallbackView.getWidth(), mCallbackView.getHeight()); 977 } 978 979 public void onNothingSelected(AdapterView<?> parent) { 980 981 } 982 }; 983 984 private View.OnClickListener mCallbackToggleListener = 985 new View.OnClickListener() { 986 public void onClick(View v) { 987 if (mCallbacksEnabled) { 988 log("Disabling preview callbacks"); 989 stopCallbacks(); 990 mCallbacksEnabled = false; 991 resizePreview(); 992 mCallbackView.setVisibility(View.GONE); 993 994 } else { 995 log("Enabling preview callbacks"); 996 mCallbacksEnabled = true; 997 resizePreview(); 998 mCallbackView.setVisibility(View.VISIBLE); 999 } 1000 } 1001 }; 1002 1003 1004 // Internal methods 1005 setUpCamera()1006 void setUpCamera() { 1007 if (mCameraId == NO_CAMERA_ID) return; 1008 1009 log("Setting up camera " + mCameraId); 1010 logIndent(1); 1011 1012 if (mState < CAMERA_OPEN) { 1013 log("Opening camera " + mCameraId); 1014 1015 if (checkSelfPermission(Manifest.permission.CAMERA) 1016 != PackageManager.PERMISSION_GRANTED) { 1017 log("Requested camera permission"); 1018 requestPermissions(new String[] {Manifest.permission.CAMERA}, 1019 PERMISSIONS_REQUEST_CAMERA); 1020 return; 1021 } 1022 1023 1024 try { 1025 mCamera = Camera.open(mCameraId); 1026 } catch (RuntimeException e) { 1027 logE("Exception opening camera: " + e.getMessage()); 1028 resetCamera(); 1029 mCameraSpinner.setSelection(0); 1030 logIndent(-1); 1031 return; 1032 } 1033 mState = CAMERA_OPEN; 1034 } 1035 1036 mCamera.setErrorCallback(this); 1037 1038 setCameraDisplayOrientation(); 1039 mParams = mCamera.getParameters(); 1040 mHDRToggle.setEnabled(false); 1041 if (mParams != null) { 1042 List<String> sceneModes = mParams.getSupportedSceneModes(); 1043 if (sceneModes != null) { 1044 for (String mode : sceneModes) { 1045 if (Camera.Parameters.SCENE_MODE_HDR.equals(mode)){ 1046 mHDRToggle.setEnabled(true); 1047 break; 1048 } 1049 } 1050 } else { 1051 Log.i(TAG, "Supported scene modes is null"); 1052 } 1053 } 1054 1055 // Set up preview size selection 1056 1057 log("Configuring camera"); 1058 logIndent(1); 1059 1060 updatePreviewSizes(mParams); 1061 updatePreviewFrameRate(mCameraId); 1062 updatePreviewFormats(mParams); 1063 updateAfModes(mParams); 1064 updateFlashModes(mParams); 1065 updateSnapshotSizes(mParams); 1066 updateCamcorderProfile(mCameraId); 1067 updateVideoRecordSize(mCameraId); 1068 updateVideoFrameRate(mCameraId); 1069 updateColorEffects(mParams); 1070 1071 // Trigger updating video record size to match camcorder profile 1072 if (mCamcorderProfile >= 0) { 1073 mCamcorderProfileSpinner.setSelection(mCamcorderProfile); 1074 } 1075 1076 if (mParams.isVideoStabilizationSupported()) { 1077 log("Video stabilization is supported"); 1078 mRecordStabilizationToggle.setEnabled(true); 1079 } else { 1080 log("Video stabilization not supported"); 1081 mRecordStabilizationToggle.setEnabled(false); 1082 } 1083 1084 if (mParams.isAutoExposureLockSupported()) { 1085 log("Auto-Exposure locking is supported"); 1086 mExposureLockToggle.setEnabled(true); 1087 } else { 1088 log("Auto-Exposure locking is not supported"); 1089 mExposureLockToggle.setEnabled(false); 1090 } 1091 1092 if (mParams.isZoomSupported()) { 1093 int maxZoom = mParams.getMaxZoom(); 1094 mZoomSeekBar.setMax(maxZoom); 1095 log("Zoom is supported, set max to " + maxZoom); 1096 mZoomSeekBar.setEnabled(true); 1097 } else { 1098 log("Zoom is not supported"); 1099 mZoomSeekBar.setEnabled(false); 1100 } 1101 1102 // Update parameters based on above updates 1103 mCamera.setParameters(mParams); 1104 1105 if (mPreviewHolder != null) { 1106 log("Setting preview display"); 1107 try { 1108 mCamera.setPreviewDisplay(mPreviewHolder); 1109 } catch(IOException e) { 1110 Log.e(TAG, "Unable to set up preview!"); 1111 } 1112 } 1113 1114 logIndent(-1); 1115 1116 enableOpenOnlyControls(true); 1117 1118 resizePreview(); 1119 if (mPreviewToggle.isChecked()) { 1120 log("Starting preview" ); 1121 mCamera.startPreview(); 1122 mState = CAMERA_PREVIEW; 1123 } else { 1124 mState = CAMERA_OPEN; 1125 enablePreviewOnlyControls(false); 1126 } 1127 logIndent(-1); 1128 } 1129 resetCamera()1130 private void resetCamera() { 1131 if (mState >= CAMERA_OPEN) { 1132 log("Closing old camera"); 1133 mCamera.release(); 1134 } 1135 mCamera = null; 1136 mCameraId = NO_CAMERA_ID; 1137 mState = CAMERA_UNINITIALIZED; 1138 1139 enableOpenOnlyControls(false); 1140 } 1141 updateAfModes(Parameters params)1142 private void updateAfModes(Parameters params) { 1143 mAfModes = params.getSupportedFocusModes(); 1144 1145 mAutofocusModeSpinner.setAdapter( 1146 new ArrayAdapter<String>(this, R.layout.spinner_item, 1147 mAfModes.toArray(new String[0]))); 1148 1149 mAfMode = 0; 1150 1151 params.setFocusMode(mAfModes.get(mAfMode)); 1152 1153 log("Setting AF mode to " + mAfModes.get(mAfMode)); 1154 } 1155 updateFlashModes(Parameters params)1156 private void updateFlashModes(Parameters params) { 1157 mFlashModes = params.getSupportedFlashModes(); 1158 1159 if (mFlashModes != null) { 1160 mFlashModeSpinnerLabel.setVisibility(View.VISIBLE); 1161 mFlashModeSpinner.setVisibility(View.VISIBLE); 1162 mFlashModeSpinner.setAdapter( 1163 new ArrayAdapter<String>(this, R.layout.spinner_item, 1164 mFlashModes.toArray(new String[0]))); 1165 1166 mFlashMode = 0; 1167 1168 params.setFlashMode(mFlashModes.get(mFlashMode)); 1169 1170 log("Setting Flash mode to " + mFlashModes.get(mFlashMode)); 1171 } else { 1172 // this camera has no flash 1173 mFlashModeSpinnerLabel.setVisibility(View.GONE); 1174 mFlashModeSpinner.setVisibility(View.GONE); 1175 } 1176 } 1177 1178 private View.OnClickListener mExposureLockToggleListener = 1179 new View.OnClickListener() { 1180 public void onClick(View v) { 1181 boolean on = ((ToggleButton) v).isChecked(); 1182 log("Auto-Exposure was " + mParams.getAutoExposureLock()); 1183 mParams.setAutoExposureLock(on); 1184 log("Auto-Exposure is now " + mParams.getAutoExposureLock()); 1185 } 1186 }; 1187 1188 private final SeekBar.OnSeekBarChangeListener mZoomSeekBarListener = 1189 new SeekBar.OnSeekBarChangeListener() { 1190 @Override 1191 public void onProgressChanged(SeekBar seekBar, int progress, 1192 boolean fromUser) { 1193 mZoom = progress; 1194 mParams.setZoom(mZoom); 1195 mCamera.setParameters(mParams); 1196 } 1197 @Override 1198 public void onStartTrackingTouch(SeekBar seekBar) { } 1199 @Override 1200 public void onStopTrackingTouch(SeekBar seekBar) { 1201 log("Zoom set to " + mZoom + " / " + mParams.getMaxZoom() + " (" + 1202 ((float)(mParams.getZoomRatios().get(mZoom))/100) + "x)"); 1203 } 1204 }; 1205 updatePreviewSizes(Camera.Parameters params)1206 private void updatePreviewSizes(Camera.Parameters params) { 1207 mPreviewSizes = params.getSupportedPreviewSizes(); 1208 1209 String[] availableSizeNames = new String[mPreviewSizes.size()]; 1210 int i = 0; 1211 for (Camera.Size previewSize: mPreviewSizes) { 1212 availableSizeNames[i++] = 1213 Integer.toString(previewSize.width) + " x " + 1214 Integer.toString(previewSize.height); 1215 } 1216 mPreviewSizeSpinner.setAdapter( 1217 new ArrayAdapter<String>( 1218 this, R.layout.spinner_item, availableSizeNames)); 1219 1220 mPreviewSize = 0; 1221 1222 int width = mPreviewSizes.get(mPreviewSize).width; 1223 int height = mPreviewSizes.get(mPreviewSize).height; 1224 params.setPreviewSize(width, height); 1225 log("Setting preview size to " + width + " x " + height); 1226 } 1227 updatePreviewFrameRate(int cameraId)1228 private void updatePreviewFrameRate(int cameraId) { 1229 List<Integer> frameRates = mParams.getSupportedPreviewFrameRates(); 1230 int defaultPreviewFrameRate = mParams.getPreviewFrameRate(); 1231 1232 List<String> frameRateStrings = new ArrayList<String>(); 1233 mPreviewFrameRates = new ArrayList<Integer>(); 1234 1235 int currentIndex = 0; 1236 for (Integer frameRate : frameRates) { 1237 mPreviewFrameRates.add(frameRate); 1238 if(frameRate == defaultPreviewFrameRate) { 1239 frameRateStrings.add(frameRate.toString() + " (Default)"); 1240 mPreviewFrameRate = currentIndex; 1241 } else { 1242 frameRateStrings.add(frameRate.toString()); 1243 } 1244 currentIndex++; 1245 } 1246 1247 String[] nameArray = (String[])frameRateStrings.toArray(new String[0]); 1248 mPreviewFrameRateSpinner.setAdapter( 1249 new ArrayAdapter<String>( 1250 this, R.layout.spinner_item, nameArray)); 1251 1252 mPreviewFrameRateSpinner.setSelection(mPreviewFrameRate); 1253 log("Setting preview frame rate to " + nameArray[mPreviewFrameRate]); 1254 } 1255 updatePreviewFormats(Camera.Parameters params)1256 private void updatePreviewFormats(Camera.Parameters params) { 1257 mPreviewFormats = params.getSupportedPreviewFormats(); 1258 1259 String[] availableFormatNames = new String[mPreviewFormats.size()]; 1260 int i = 0; 1261 for (Integer previewFormat: mPreviewFormats) { 1262 availableFormatNames[i++] = mFormatNames.get(previewFormat); 1263 } 1264 mCallbackFormatSpinner.setAdapter( 1265 new ArrayAdapter<String>( 1266 this, R.layout.spinner_item, availableFormatNames)); 1267 1268 mPreviewFormat = 0; 1269 mCallbacksEnabled = false; 1270 mCallbackToggle.setChecked(false); 1271 mCallbackView.setVisibility(View.GONE); 1272 1273 params.setPreviewFormat(mPreviewFormats.get(mPreviewFormat)); 1274 log("Setting preview format to " + 1275 mFormatNames.get(mPreviewFormats.get(mPreviewFormat))); 1276 } 1277 updateSnapshotSizes(Camera.Parameters params)1278 private void updateSnapshotSizes(Camera.Parameters params) { 1279 String[] availableSizeNames; 1280 mSnapshotSizes = params.getSupportedPictureSizes(); 1281 1282 availableSizeNames = new String[mSnapshotSizes.size()]; 1283 int i = 0; 1284 for (Camera.Size snapshotSize : mSnapshotSizes) { 1285 availableSizeNames[i++] = 1286 Integer.toString(snapshotSize.width) + " x " + 1287 Integer.toString(snapshotSize.height); 1288 } 1289 mSnapshotSizeSpinner.setAdapter( 1290 new ArrayAdapter<String>( 1291 this, R.layout.spinner_item, availableSizeNames)); 1292 1293 mSnapshotSize = 0; 1294 1295 int snapshotWidth = mSnapshotSizes.get(mSnapshotSize).width; 1296 int snapshotHeight = mSnapshotSizes.get(mSnapshotSize).height; 1297 params.setPictureSize(snapshotWidth, snapshotHeight); 1298 log("Setting snapshot size to " + snapshotWidth + " x " + snapshotHeight); 1299 } 1300 updateCamcorderProfile(int cameraId)1301 private void updateCamcorderProfile(int cameraId) { 1302 // Have to query all of these individually, 1303 final int PROFILES[] = new int[] { 1304 CamcorderProfile.QUALITY_2160P, 1305 CamcorderProfile.QUALITY_1080P, 1306 CamcorderProfile.QUALITY_480P, 1307 CamcorderProfile.QUALITY_720P, 1308 CamcorderProfile.QUALITY_CIF, 1309 CamcorderProfile.QUALITY_HIGH, 1310 CamcorderProfile.QUALITY_LOW, 1311 CamcorderProfile.QUALITY_QCIF, 1312 CamcorderProfile.QUALITY_QVGA, 1313 CamcorderProfile.QUALITY_TIME_LAPSE_2160P, 1314 CamcorderProfile.QUALITY_TIME_LAPSE_1080P, 1315 CamcorderProfile.QUALITY_TIME_LAPSE_480P, 1316 CamcorderProfile.QUALITY_TIME_LAPSE_720P, 1317 CamcorderProfile.QUALITY_TIME_LAPSE_CIF, 1318 CamcorderProfile.QUALITY_TIME_LAPSE_HIGH, 1319 CamcorderProfile.QUALITY_TIME_LAPSE_LOW, 1320 CamcorderProfile.QUALITY_TIME_LAPSE_QCIF, 1321 CamcorderProfile.QUALITY_TIME_LAPSE_QVGA 1322 }; 1323 1324 final String PROFILE_NAMES[] = new String[] { 1325 "2160P", 1326 "1080P", 1327 "480P", 1328 "720P", 1329 "CIF", 1330 "HIGH", 1331 "LOW", 1332 "QCIF", 1333 "QVGA", 1334 "TIME_LAPSE_2160P", 1335 "TIME_LAPSE_1080P", 1336 "TIME_LAPSE_480P", 1337 "TIME_LAPSE_720P", 1338 "TIME_LAPSE_CIF", 1339 "TIME_LAPSE_HIGH", 1340 "TIME_LAPSE_LOW", 1341 "TIME_LAPSE_QCIF", 1342 "TIME_LAPSE_QVGA" 1343 }; 1344 1345 List<String> availableCamcorderProfileNames = new ArrayList<String>(); 1346 mCamcorderProfiles = new ArrayList<CamcorderProfile>(); 1347 1348 for (int i = 0; i < PROFILES.length; i++) { 1349 if (CamcorderProfile.hasProfile(cameraId, PROFILES[i])) { 1350 availableCamcorderProfileNames.add(PROFILE_NAMES[i]); 1351 mCamcorderProfiles.add(CamcorderProfile.get(cameraId, PROFILES[i])); 1352 } 1353 } 1354 1355 String[] nameArray = (String[])availableCamcorderProfileNames.toArray(new String[0]); 1356 mCamcorderProfileSpinner.setAdapter( 1357 new ArrayAdapter<String>( 1358 this, R.layout.spinner_item, nameArray)); 1359 1360 if (availableCamcorderProfileNames.size() == 0) { 1361 log("Camera " + cameraId + " doesn't support camcorder profile"); 1362 mCamcorderProfile = -1; 1363 return; 1364 } 1365 1366 mCamcorderProfile = 0; 1367 log("Setting camcorder profile to " + nameArray[mCamcorderProfile]); 1368 1369 } 1370 updateVideoRecordSize(int cameraId)1371 private void updateVideoRecordSize(int cameraId) { 1372 List<Camera.Size> videoSizes = mParams.getSupportedVideoSizes(); 1373 if (videoSizes == null) { // TODO: surface this to the user 1374 log("Failed to get video size list, using preview sizes instead"); 1375 videoSizes = mParams.getSupportedPreviewSizes(); 1376 } 1377 1378 List<String> availableVideoRecordSizes = new ArrayList<String>(); 1379 mVideoRecordSizes = new ArrayList<Camera.Size>(); 1380 1381 availableVideoRecordSizes.add("Default"); 1382 mVideoRecordSizes.add(mCamera.new Size(0,0)); 1383 1384 for (Camera.Size s : videoSizes) { 1385 availableVideoRecordSizes.add(s.width + "x" + s.height); 1386 mVideoRecordSizes.add(s); 1387 } 1388 String[] nameArray = (String[])availableVideoRecordSizes.toArray(new String[0]); 1389 mVideoRecordSizeSpinner.setAdapter( 1390 new ArrayAdapter<String>( 1391 this, R.layout.spinner_item, nameArray)); 1392 1393 mVideoRecordSize = 0; 1394 log("Setting video record profile to " + nameArray[mVideoRecordSize]); 1395 } 1396 updateVideoFrameRate(int cameraId)1397 private void updateVideoFrameRate(int cameraId) { 1398 // Use preview framerates as video framerates 1399 List<Integer> frameRates = mParams.getSupportedPreviewFrameRates(); 1400 1401 List<String> frameRateStrings = new ArrayList<String>(); 1402 mVideoFrameRates = new ArrayList<Integer>(); 1403 1404 frameRateStrings.add("Default"); 1405 mVideoFrameRates.add(0); 1406 1407 for (Integer frameRate : frameRates) { 1408 frameRateStrings.add(frameRate.toString()); 1409 mVideoFrameRates.add(frameRate); 1410 } 1411 String[] nameArray = (String[])frameRateStrings.toArray(new String[0]); 1412 mVideoFrameRateSpinner.setAdapter( 1413 new ArrayAdapter<String>( 1414 this, R.layout.spinner_item, nameArray)); 1415 1416 mVideoFrameRate = 0; 1417 log("Setting recording frame rate to " + nameArray[mVideoFrameRate]); 1418 } 1419 resizePreview()1420 void resizePreview() { 1421 // Reset preview layout parameters, to trigger layout pass 1422 // This will eventually call layoutPreview below 1423 Resources res = getResources(); 1424 mPreviewView.setLayoutParams( 1425 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0, 1426 mCallbacksEnabled ? 1427 res.getInteger(R.integer.preview_with_callback_weight): 1428 res.getInteger(R.integer.preview_only_weight) )); 1429 } 1430 layoutPreview()1431 void layoutPreview() { 1432 int rotation = getWindowManager().getDefaultDisplay().getRotation(); 1433 int width = mPreviewSizes.get(mPreviewSize).width; 1434 int height = mPreviewSizes.get(mPreviewSize).height; 1435 switch (rotation) { 1436 case Surface.ROTATION_0: 1437 case Surface.ROTATION_180: 1438 // Portrait 1439 // Switch the preview size so that the longer edge aligns with the taller 1440 // dimension. 1441 if (width > height) { 1442 int tmp = height; 1443 height = width; 1444 width = tmp; 1445 } 1446 break; 1447 case Surface.ROTATION_90: 1448 case Surface.ROTATION_270: 1449 // Landscape 1450 // Possibly somewhat unlikely case but we should try to handle it too. 1451 if (height > width) { 1452 int tmp = height; 1453 height = width; 1454 width = tmp; 1455 } 1456 break; 1457 } 1458 1459 float previewAspect = ((float) width) / height; 1460 1461 int viewHeight = mPreviewView.getHeight(); 1462 int viewWidth = mPreviewView.getWidth(); 1463 float viewAspect = ((float) viewWidth) / viewHeight; 1464 if ( previewAspect > viewAspect) { 1465 viewHeight = (int) (viewWidth / previewAspect); 1466 } else { 1467 viewWidth = (int) (viewHeight * previewAspect); 1468 } 1469 mPreviewView.setLayoutParams( 1470 new LayoutParams(viewWidth, viewHeight)); 1471 log("Setting layout params viewWidth: " + viewWidth + " viewHeight: " + viewHeight + 1472 " display rotation: " + rotation); 1473 1474 if (mCallbacksEnabled) { 1475 int callbackHeight = mCallbackView.getHeight(); 1476 int callbackWidth = mCallbackView.getWidth(); 1477 float callbackAspect = ((float) callbackWidth) / callbackHeight; 1478 if ( previewAspect > callbackAspect) { 1479 callbackHeight = (int) (callbackWidth / previewAspect); 1480 } else { 1481 callbackWidth = (int) (callbackHeight * previewAspect); 1482 } 1483 mCallbackView.setLayoutParams( 1484 new LayoutParams(callbackWidth, callbackHeight)); 1485 configureCallbacks(callbackWidth, callbackHeight); 1486 } 1487 } 1488 1489 configureCallbacks(int callbackWidth, int callbackHeight)1490 private void configureCallbacks(int callbackWidth, int callbackHeight) { 1491 if (mState >= CAMERA_OPEN && mCallbacksEnabled) { 1492 mCamera.setPreviewCallbackWithBuffer(null); 1493 int width = mPreviewSizes.get(mPreviewSize).width; 1494 int height = mPreviewSizes.get(mPreviewSize).height; 1495 int format = mPreviewFormats.get(mPreviewFormat); 1496 1497 mCallbackProcessor = new CallbackProcessor(width, height, format, 1498 getResources(), mCallbackView, 1499 callbackWidth, callbackHeight, mRS); 1500 1501 int size = getCallbackBufferSize(width, height, format); 1502 log("Configuring callbacks:" + width + " x " + height + 1503 " , format " + format); 1504 for (int i = 0; i < CALLBACK_BUFFER_COUNT; i++) { 1505 mCamera.addCallbackBuffer(new byte[size]); 1506 } 1507 mCamera.setPreviewCallbackWithBuffer(this); 1508 } 1509 mLastCallbackTimestamp = -1; 1510 mCallbackFrameCount = 0; 1511 mCallbackAvgFrameDuration = 30; 1512 } 1513 stopCallbacks()1514 private void stopCallbacks() { 1515 if (mState >= CAMERA_OPEN) { 1516 mCamera.setPreviewCallbackWithBuffer(null); 1517 if (mCallbackProcessor != null) { 1518 if (!mCallbackProcessor.stop()) { 1519 logE("Can't stop preview callback processing!"); 1520 } 1521 } 1522 } 1523 } 1524 1525 @Override onPreviewFrame(byte[] data, Camera camera)1526 public void onPreviewFrame(byte[] data, Camera camera) { 1527 long timestamp = SystemClock.elapsedRealtime(); 1528 if (mLastCallbackTimestamp != -1) { 1529 long frameDuration = timestamp - mLastCallbackTimestamp; 1530 mCallbackAvgFrameDuration = 1531 mCallbackAvgFrameDuration * MEAN_FPS_HISTORY_COEFF + 1532 frameDuration * MEAN_FPS_MEASUREMENT_COEFF; 1533 } 1534 mLastCallbackTimestamp = timestamp; 1535 if (mState < CAMERA_PREVIEW || !mCallbacksEnabled) { 1536 mCamera.addCallbackBuffer(data); 1537 return; 1538 } 1539 mCallbackFrameCount++; 1540 if (mCallbackFrameCount % FPS_REPORTING_PERIOD == 0) { 1541 log("Got " + FPS_REPORTING_PERIOD + " callback frames, fps " 1542 + 1e3/mCallbackAvgFrameDuration); 1543 } 1544 mCallbackProcessor.displayCallback(data); 1545 1546 mCamera.addCallbackBuffer(data); 1547 } 1548 1549 @Override onError(int error, Camera camera)1550 public void onError(int error, Camera camera) { 1551 String errorName; 1552 switch (error) { 1553 case Camera.CAMERA_ERROR_SERVER_DIED: 1554 errorName = "SERVER_DIED"; 1555 break; 1556 case Camera.CAMERA_ERROR_UNKNOWN: 1557 errorName = "UNKNOWN"; 1558 break; 1559 default: 1560 errorName = "?"; 1561 break; 1562 } 1563 logE("Camera error received: " + errorName + " (" + error + ")" ); 1564 logE("Shutting down camera"); 1565 resetCamera(); 1566 mCameraSpinner.setSelection(0); 1567 } 1568 1569 static final int MEDIA_TYPE_IMAGE = 0; 1570 static final int MEDIA_TYPE_VIDEO = 1; 1571 @SuppressLint("SimpleDateFormat") getOutputMediaFile(int type)1572 File getOutputMediaFile(int type){ 1573 // To be safe, you should check that the SDCard is mounted 1574 // using Environment.getExternalStorageState() before doing this. 1575 1576 String state = Environment.getExternalStorageState(); 1577 if (!Environment.MEDIA_MOUNTED.equals(state)) { 1578 return null; 1579 } 1580 1581 File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory( 1582 Environment.DIRECTORY_DCIM), "TestingCamera"); 1583 // This location works best if you want the created images to be shared 1584 // between applications and persist after your app has been uninstalled. 1585 1586 // Create the storage directory if it does not exist 1587 if (! mediaStorageDir.exists()){ 1588 if (! mediaStorageDir.mkdirs()){ 1589 logE("Failed to create directory for pictures/video"); 1590 return null; 1591 } 1592 } 1593 1594 // Create a media file name 1595 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 1596 File mediaFile; 1597 if (type == MEDIA_TYPE_IMAGE){ 1598 mediaFile = new File(mediaStorageDir.getPath() + File.separator + 1599 "IMG_"+ timeStamp + ".jpg"); 1600 } else if(type == MEDIA_TYPE_VIDEO) { 1601 mediaFile = new File(mediaStorageDir.getPath() + File.separator + 1602 "VID_"+ timeStamp + ".mp4"); 1603 } else { 1604 return null; 1605 } 1606 1607 return mediaFile; 1608 } 1609 notifyMediaScannerOfFile(File newFile, final MediaScannerConnection.OnScanCompletedListener listener)1610 void notifyMediaScannerOfFile(File newFile, 1611 final MediaScannerConnection.OnScanCompletedListener listener) { 1612 final Handler h = new Handler(); 1613 MediaScannerConnection.scanFile(this, 1614 new String[] { newFile.toString() }, 1615 null, 1616 new MediaScannerConnection.OnScanCompletedListener() { 1617 @Override 1618 public void onScanCompleted(final String path, final Uri uri) { 1619 h.post(new Runnable() { 1620 @Override 1621 public void run() { 1622 log("MediaScanner notified: " + 1623 path + " -> " + uri); 1624 if (listener != null) 1625 listener.onScanCompleted(path, uri); 1626 } 1627 }); 1628 } 1629 }); 1630 } 1631 deleteFile(File badFile)1632 private void deleteFile(File badFile) { 1633 if (badFile.exists()) { 1634 boolean success = badFile.delete(); 1635 if (success) log("Deleted file " + badFile.toString()); 1636 else log("Unable to delete file " + badFile.toString()); 1637 } 1638 } 1639 1640 private static final int BIT_RATE_1080P = 16000000; 1641 private static final int BIT_RATE_MIN = 64000; 1642 private static final int BIT_RATE_MAX = 40000000; 1643 getVideoBitRate(Camera.Size sz)1644 private int getVideoBitRate(Camera.Size sz) { 1645 int rate = BIT_RATE_1080P; 1646 float scaleFactor = sz.height * sz.width / (float)(1920 * 1080); 1647 rate = (int)(rate * scaleFactor); 1648 1649 // Clamp to the MIN, MAX range. 1650 return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate)); 1651 } 1652 startRecording()1653 private void startRecording() { 1654 log("Starting recording"); 1655 1656 if ((checkSelfPermission(Manifest.permission.RECORD_AUDIO) 1657 != PackageManager.PERMISSION_GRANTED) 1658 || (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) 1659 != PackageManager.PERMISSION_GRANTED)) { 1660 log("Requesting recording permissions (audio, storage)"); 1661 requestPermissions(new String[] { 1662 Manifest.permission.RECORD_AUDIO, 1663 Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1664 PERMISSIONS_REQUEST_RECORDING); 1665 return; 1666 } 1667 1668 logIndent(1); 1669 log("Configuring MediaRecoder"); 1670 1671 mRecordHandoffCheckBox.setEnabled(false); 1672 if (mRecordHandoffCheckBox.isChecked()) { 1673 mCamera.release(); 1674 } else { 1675 mCamera.unlock(); 1676 } 1677 1678 if (mRecorder != null) { 1679 mRecorder.release(); 1680 } 1681 1682 mRecorder = new MediaRecorder(); 1683 mRecorder.setOnErrorListener(mRecordingErrorListener); 1684 mRecorder.setOnInfoListener(mRecordingInfoListener); 1685 if (!mRecordHandoffCheckBox.isChecked()) { 1686 mRecorder.setCamera(mCamera); 1687 } 1688 mRecorder.setPreviewDisplay(mPreviewHolder.getSurface()); 1689 1690 mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); 1691 mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 1692 1693 Camera.Size videoRecordSize = mVideoRecordSizes.get(mVideoRecordSize); 1694 if (mCamcorderProfile >= 0) { 1695 mRecorder.setProfile(mCamcorderProfiles.get(mCamcorderProfile)); 1696 } else { 1697 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 1698 mRecorder.setVideoEncodingBitRate(getVideoBitRate(videoRecordSize)); 1699 mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 1700 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); 1701 } 1702 1703 if (videoRecordSize.width > 0 && videoRecordSize.height > 0) { 1704 mRecorder.setVideoSize(videoRecordSize.width, videoRecordSize.height); 1705 } 1706 if (mVideoFrameRates.get(mVideoFrameRate) > 0) { 1707 mRecorder.setVideoFrameRate(mVideoFrameRates.get(mVideoFrameRate)); 1708 } 1709 File outputFile = getOutputMediaFile(MEDIA_TYPE_VIDEO); 1710 log("File name:" + outputFile.toString()); 1711 mRecorder.setOutputFile(outputFile.toString()); 1712 1713 boolean ready = false; 1714 log("Preparing MediaRecorder"); 1715 try { 1716 mRecorder.prepare(); 1717 ready = true; 1718 } catch (Exception e) { 1719 StringWriter writer = new StringWriter(); 1720 e.printStackTrace(new PrintWriter(writer)); 1721 logE("Exception preparing MediaRecorder:\n" + writer.toString()); 1722 } 1723 1724 if (ready) { 1725 try { 1726 log("Starting MediaRecorder"); 1727 mRecorder.start(); 1728 mState = CAMERA_RECORD; 1729 log("Recording active"); 1730 mRecordingFile = outputFile; 1731 } catch (Exception e) { 1732 StringWriter writer = new StringWriter(); 1733 e.printStackTrace(new PrintWriter(writer)); 1734 logE("Exception starting MediaRecorder:\n" + writer.toString()); 1735 ready = false; 1736 } 1737 } 1738 1739 if (!ready) { 1740 mRecordToggle.setChecked(false); 1741 mRecordHandoffCheckBox.setEnabled(true); 1742 1743 if (mRecordHandoffCheckBox.isChecked()) { 1744 mState = CAMERA_UNINITIALIZED; 1745 setUpCamera(); 1746 } 1747 } 1748 logIndent(-1); 1749 } 1750 1751 private MediaRecorder.OnErrorListener mRecordingErrorListener = 1752 new MediaRecorder.OnErrorListener() { 1753 @Override 1754 public void onError(MediaRecorder mr, int what, int extra) { 1755 logE("MediaRecorder reports error: " + what + ", extra " 1756 + extra); 1757 if (mState == CAMERA_RECORD) { 1758 stopRecording(true); 1759 } 1760 } 1761 }; 1762 1763 private MediaRecorder.OnInfoListener mRecordingInfoListener = 1764 new MediaRecorder.OnInfoListener() { 1765 @Override 1766 public void onInfo(MediaRecorder mr, int what, int extra) { 1767 log("MediaRecorder reports info: " + what + ", extra " 1768 + extra); 1769 } 1770 }; 1771 stopRecording(boolean error)1772 private void stopRecording(boolean error) { 1773 log("Stopping recording"); 1774 mRecordHandoffCheckBox.setEnabled(true); 1775 mRecordToggle.setChecked(false); 1776 if (mRecorder != null) { 1777 try { 1778 mRecorder.stop(); 1779 } catch (RuntimeException e) { 1780 // this can happen if there were no frames received by recorder 1781 logE("Could not create output file"); 1782 error = true; 1783 } 1784 1785 if (mRecordHandoffCheckBox.isChecked()) { 1786 mState = CAMERA_UNINITIALIZED; 1787 setUpCamera(); 1788 } else { 1789 mCamera.lock(); 1790 mState = CAMERA_PREVIEW; 1791 } 1792 1793 if (!error) { 1794 notifyMediaScannerOfFile(mRecordingFile, null); 1795 } else { 1796 deleteFile(mRecordingFile); 1797 } 1798 mRecordingFile = null; 1799 } else { 1800 logE("Recorder is unexpectedly null!"); 1801 } 1802 } 1803 getCallbackBufferSize(int width, int height, int format)1804 static int getCallbackBufferSize(int width, int height, int format) { 1805 int size = -1; 1806 switch (format) { 1807 case ImageFormat.NV21: 1808 size = width * height * 3 / 2; 1809 break; 1810 case ImageFormat.YV12: 1811 int y_stride = (int) (Math.ceil( width / 16.) * 16); 1812 int y_size = y_stride * height; 1813 int c_stride = (int) (Math.ceil(y_stride / 32.) * 16); 1814 int c_size = c_stride * height/2; 1815 size = y_size + c_size * 2; 1816 break; 1817 case ImageFormat.NV16: 1818 case ImageFormat.RGB_565: 1819 case ImageFormat.YUY2: 1820 size = 2 * width * height; 1821 break; 1822 case ImageFormat.JPEG: 1823 Log.e(TAG, "JPEG callback buffers not supported!"); 1824 size = 0; 1825 break; 1826 case ImageFormat.UNKNOWN: 1827 Log.e(TAG, "Unknown-format callback buffers not supported!"); 1828 size = 0; 1829 break; 1830 } 1831 return size; 1832 } 1833 1834 private OnItemSelectedListener mColorEffectListener = 1835 new OnItemSelectedListener() { 1836 @Override 1837 public void onItemSelected(AdapterView<?> parent, 1838 View view, int pos, long id) { 1839 if (pos == mColorEffect) return; 1840 1841 mColorEffect = pos; 1842 String colorEffect = mColorEffects.get(mColorEffect); 1843 log("Setting color effect to " + colorEffect); 1844 mParams.setColorEffect(colorEffect); 1845 mCamera.setParameters(mParams); 1846 } 1847 1848 @Override 1849 public void onNothingSelected(AdapterView<?> arg0) { 1850 } 1851 }; 1852 updateColorEffects(Parameters params)1853 private void updateColorEffects(Parameters params) { 1854 mColorEffects = params.getSupportedColorEffects(); 1855 if (mColorEffects != null) { 1856 mColorEffectSpinnerLabel.setVisibility(View.VISIBLE); 1857 mColorEffectSpinner.setVisibility(View.VISIBLE); 1858 mColorEffectSpinner.setAdapter( 1859 new ArrayAdapter<String>(this, R.layout.spinner_item, 1860 mColorEffects.toArray(new String[0]))); 1861 mColorEffect = 0; 1862 params.setColorEffect(mColorEffects.get(mColorEffect)); 1863 log("Setting Color Effect to " + mColorEffects.get(mColorEffect)); 1864 } else { 1865 mColorEffectSpinnerLabel.setVisibility(View.GONE); 1866 mColorEffectSpinner.setVisibility(View.GONE); 1867 } 1868 } 1869 1870 private int mLogIndentLevel = 0; 1871 private String mLogIndent = "\t"; 1872 /** Increment or decrement log indentation level */ logIndent(int delta)1873 synchronized void logIndent(int delta) { 1874 mLogIndentLevel += delta; 1875 if (mLogIndentLevel < 0) mLogIndentLevel = 0; 1876 char[] mLogIndentArray = new char[mLogIndentLevel + 1]; 1877 for (int i = -1; i < mLogIndentLevel; i++) { 1878 mLogIndentArray[i + 1] = '\t'; 1879 } 1880 mLogIndent = new String(mLogIndentArray); 1881 } 1882 1883 @SuppressLint("SimpleDateFormat") 1884 SimpleDateFormat mDateFormatter = new SimpleDateFormat("HH:mm:ss.SSS"); 1885 /** Log both to log text view and to device logcat */ log(String logLine)1886 void log(String logLine) { 1887 Log.d(TAG, logLine); 1888 logAndScrollToBottom(logLine, mLogIndent); 1889 } 1890 logE(String logLine)1891 void logE(String logLine) { 1892 Log.e(TAG, logLine); 1893 logAndScrollToBottom(logLine, mLogIndent + "!!! "); 1894 } 1895 logAndScrollToBottom(String logLine, String logIndent)1896 synchronized private void logAndScrollToBottom(String logLine, String logIndent) { 1897 StringBuffer logEntry = new StringBuffer(32); 1898 logEntry.append("\n").append(mDateFormatter.format(new Date())).append(logIndent); 1899 logEntry.append(logLine); 1900 mLogView.append(logEntry); 1901 final Layout layout = mLogView.getLayout(); 1902 if (layout != null){ 1903 int scrollDelta = layout.getLineBottom(mLogView.getLineCount() - 1) 1904 - mLogView.getScrollY() - mLogView.getHeight(); 1905 if(scrollDelta > 0) { 1906 mLogView.scrollBy(0, scrollDelta); 1907 } 1908 } 1909 } 1910 } 1911