1 /* 2 * Copyright (C) 2015 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.messaging.ui.mediapicker; 18 19 import android.Manifest; 20 import android.app.Activity; 21 import android.content.Context; 22 import android.content.pm.ActivityInfo; 23 import android.content.res.Configuration; 24 import android.content.res.Resources; 25 import android.hardware.Camera; 26 import android.hardware.Camera.CameraInfo; 27 import android.media.MediaRecorder; 28 import android.net.Uri; 29 import android.os.AsyncTask; 30 import android.os.Looper; 31 import android.support.annotation.NonNull; 32 import android.text.TextUtils; 33 import android.util.DisplayMetrics; 34 import android.view.MotionEvent; 35 import android.view.OrientationEventListener; 36 import android.view.Surface; 37 import android.view.View; 38 import android.view.WindowManager; 39 40 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; 41 import com.android.messaging.Factory; 42 import com.android.messaging.datamodel.data.ParticipantData; 43 import com.android.messaging.datamodel.media.ImageRequest; 44 import com.android.messaging.sms.MmsConfig; 45 import com.android.messaging.ui.mediapicker.camerafocus.FocusOverlayManager; 46 import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay; 47 import com.android.messaging.util.Assert; 48 import com.android.messaging.util.BugleGservices; 49 import com.android.messaging.util.BugleGservicesKeys; 50 import com.android.messaging.util.LogUtil; 51 import com.android.messaging.util.OsUtil; 52 import com.android.messaging.util.UiUtils; 53 import com.google.common.annotations.VisibleForTesting; 54 55 import java.io.FileNotFoundException; 56 import java.io.IOException; 57 import java.util.ArrayList; 58 import java.util.Collections; 59 import java.util.Comparator; 60 import java.util.List; 61 62 /** 63 * Class which manages interactions with the camera, but does not do any UI. This class is 64 * designed to be a singleton to ensure there is one component managing the camera and releasing 65 * the native resources. 66 * In order to acquire a camera, a caller must: 67 * <ul> 68 * <li>Call selectCamera to select front or back camera 69 * <li>Call setSurface to control where the preview is shown 70 * <li>Call openCamera to request the camera start preview 71 * </ul> 72 * Callers should call onPause and onResume to ensure that the camera is release while the activity 73 * is not active. 74 * This class is not thread safe. It should only be called from one thread (the UI thread or test 75 * thread) 76 */ 77 class CameraManager implements FocusOverlayManager.Listener { 78 /** 79 * Wrapper around the framework camera API to allow mocking different hardware scenarios while 80 * unit testing 81 */ 82 interface CameraWrapper { 83 int getNumberOfCameras(); 84 void getCameraInfo(int index, CameraInfo cameraInfo); 85 Camera open(int cameraId); 86 /** Add a wrapper for release because a final method cannot be mocked */ 87 void release(Camera camera); 88 } 89 90 /** 91 * Callbacks for the camera manager listener 92 */ 93 interface CameraManagerListener { 94 void onCameraError(int errorCode, Exception e); 95 void onCameraChanged(); 96 } 97 98 /** 99 * Callback when taking image or video 100 */ 101 interface MediaCallback { 102 static final int MEDIA_CAMERA_CHANGED = 1; 103 static final int MEDIA_NO_DATA = 2; 104 105 void onMediaReady(Uri uriToMedia, String contentType, int width, int height); 106 void onMediaFailed(Exception exception); 107 void onMediaInfo(int what); 108 } 109 110 // Error codes 111 static final int ERROR_OPENING_CAMERA = 1; 112 static final int ERROR_SHOWING_PREVIEW = 2; 113 static final int ERROR_INITIALIZING_VIDEO = 3; 114 static final int ERROR_STORAGE_FAILURE = 4; 115 static final int ERROR_RECORDING_VIDEO = 5; 116 static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 6; 117 static final int ERROR_TAKING_PICTURE = 7; 118 119 private static final String TAG = LogUtil.BUGLE_TAG; 120 private static final int NO_CAMERA_SELECTED = -1; 121 122 private static CameraManager sInstance; 123 124 /** Default camera wrapper which directs calls to the framework APIs */ 125 private static CameraWrapper sCameraWrapper = new CameraWrapper() { 126 @Override 127 public int getNumberOfCameras() { 128 return Camera.getNumberOfCameras(); 129 } 130 131 @Override 132 public void getCameraInfo(final int index, final CameraInfo cameraInfo) { 133 Camera.getCameraInfo(index, cameraInfo); 134 } 135 136 @Override 137 public Camera open(final int cameraId) { 138 return Camera.open(cameraId); 139 } 140 141 @Override 142 public void release(final Camera camera) { 143 camera.release(); 144 } 145 }; 146 147 /** The CameraInfo for the currently selected camera */ 148 private final CameraInfo mCameraInfo; 149 150 /** 151 * The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet 152 */ 153 private int mCameraIndex; 154 155 /** True if the device has front and back cameras */ 156 private final boolean mHasFrontAndBackCamera; 157 158 /** True if the camera should be open (may not yet be actually open) */ 159 private boolean mOpenRequested; 160 161 /** True if the camera is requested to be in video mode */ 162 private boolean mVideoModeRequested; 163 164 /** The media recorder for video mode */ 165 private MmsVideoRecorder mMediaRecorder; 166 167 /** Callback to call with video recording updates */ 168 private MediaCallback mVideoCallback; 169 170 /** The preview view to show the preview on */ 171 private CameraPreview mCameraPreview; 172 173 /** The helper classs to handle orientation changes */ 174 private OrientationHandler mOrientationHandler; 175 176 /** Tracks whether the preview has hardware acceleration */ 177 private boolean mIsHardwareAccelerationSupported; 178 179 /** 180 * The task for opening the camera, so it doesn't block the UI thread 181 * Using AsyncTask rather than SafeAsyncTask because the tasks need to be serialized, but don't 182 * need to be on the UI thread 183 * TODO: If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may 184 * need to create a dedicated thread, or synchronize the threads in the thread pool 185 */ 186 private AsyncTask<Integer, Void, Camera> mOpenCameraTask; 187 188 /** 189 * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if 190 * no open task is pending 191 */ 192 private int mPendingOpenCameraIndex = NO_CAMERA_SELECTED; 193 194 /** The instance of the currently opened camera */ 195 private Camera mCamera; 196 197 /** The rotation of the screen relative to the camera's natural orientation */ 198 private int mRotation; 199 200 /** The callback to notify when errors or other events occur */ 201 private CameraManagerListener mListener; 202 203 /** True if the camera is currently in the process of taking an image */ 204 private boolean mTakingPicture; 205 206 /** Provides subscription-related data to access per-subscription configurations. */ 207 private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; 208 209 /** Manages auto focus visual and behavior */ 210 private final FocusOverlayManager mFocusOverlayManager; 211 212 private CameraManager() { 213 mCameraInfo = new CameraInfo(); 214 mCameraIndex = NO_CAMERA_SELECTED; 215 216 // Check to see if a front and back camera exist 217 boolean hasFrontCamera = false; 218 boolean hasBackCamera = false; 219 final CameraInfo cameraInfo = new CameraInfo(); 220 final int cameraCount = sCameraWrapper.getNumberOfCameras(); 221 try { 222 for (int i = 0; i < cameraCount; i++) { 223 sCameraWrapper.getCameraInfo(i, cameraInfo); 224 if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) { 225 hasFrontCamera = true; 226 } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { 227 hasBackCamera = true; 228 } 229 if (hasFrontCamera && hasBackCamera) { 230 break; 231 } 232 } 233 } catch (final RuntimeException e) { 234 LogUtil.e(TAG, "Unable to load camera info", e); 235 } 236 mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera; 237 mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper()); 238 239 // Assume the best until we are proven otherwise 240 mIsHardwareAccelerationSupported = true; 241 } 242 243 /** Gets the singleton instance */ 244 static CameraManager get() { 245 if (sInstance == null) { 246 sInstance = new CameraManager(); 247 } 248 return sInstance; 249 } 250 251 /** Allows tests to inject a custom camera wrapper */ 252 @VisibleForTesting 253 static void setCameraWrapper(final CameraWrapper cameraWrapper) { 254 sCameraWrapper = cameraWrapper; 255 sInstance = null; 256 } 257 258 /** 259 * Sets the surface to use to display the preview 260 * This must only be called AFTER the CameraPreview has a texture ready 261 * @param preview The preview surface view 262 */ 263 void setSurface(final CameraPreview preview) { 264 if (preview == mCameraPreview) { 265 return; 266 } 267 268 if (preview != null) { 269 Assert.isTrue(preview.isValid()); 270 preview.setOnTouchListener(new View.OnTouchListener() { 271 @Override 272 public boolean onTouch(final View view, final MotionEvent motionEvent) { 273 if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP) == 274 MotionEvent.ACTION_UP) { 275 mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight()); 276 mFocusOverlayManager.onSingleTapUp( 277 (int) motionEvent.getX() + view.getLeft(), 278 (int) motionEvent.getY() + view.getTop()); 279 } 280 return true; 281 } 282 }); 283 } 284 mCameraPreview = preview; 285 tryShowPreview(); 286 } 287 288 void setRenderOverlay(final RenderOverlay renderOverlay) { 289 mFocusOverlayManager.setFocusRenderer(renderOverlay != null ? 290 renderOverlay.getPieRenderer() : null); 291 } 292 293 /** Convenience function to swap between front and back facing cameras */ 294 void swapCamera() { 295 Assert.isTrue(mCameraIndex >= 0); 296 selectCamera(mCameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT ? 297 CameraInfo.CAMERA_FACING_BACK : 298 CameraInfo.CAMERA_FACING_FRONT); 299 } 300 301 /** 302 * Selects the first camera facing the desired direction, or the first camera if there is no 303 * camera in the desired direction 304 * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants 305 * @return True if a camera was selected, or false if selecting a camera failed 306 */ 307 boolean selectCamera(final int desiredFacing) { 308 try { 309 // We already selected a camera facing that direction 310 if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) { 311 return true; 312 } 313 314 final int cameraCount = sCameraWrapper.getNumberOfCameras(); 315 Assert.isTrue(cameraCount > 0); 316 317 mCameraIndex = NO_CAMERA_SELECTED; 318 setCamera(null); 319 final CameraInfo cameraInfo = new CameraInfo(); 320 for (int i = 0; i < cameraCount; i++) { 321 sCameraWrapper.getCameraInfo(i, cameraInfo); 322 if (cameraInfo.facing == desiredFacing) { 323 mCameraIndex = i; 324 sCameraWrapper.getCameraInfo(i, mCameraInfo); 325 break; 326 } 327 } 328 329 // There's no camera in the desired facing direction, just select the first camera 330 // regardless of direction 331 if (mCameraIndex < 0) { 332 mCameraIndex = 0; 333 sCameraWrapper.getCameraInfo(0, mCameraInfo); 334 } 335 336 if (mOpenRequested) { 337 // The camera is open, so reopen with the newly selected camera 338 openCamera(); 339 } 340 return true; 341 } catch (final RuntimeException e) { 342 LogUtil.e(TAG, "RuntimeException in CameraManager.selectCamera", e); 343 if (mListener != null) { 344 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 345 } 346 return false; 347 } 348 } 349 350 int getCameraIndex() { 351 return mCameraIndex; 352 } 353 354 void selectCameraByIndex(final int cameraIndex) { 355 if (mCameraIndex == cameraIndex) { 356 return; 357 } 358 359 try { 360 mCameraIndex = cameraIndex; 361 sCameraWrapper.getCameraInfo(mCameraIndex, mCameraInfo); 362 if (mOpenRequested) { 363 openCamera(); 364 } 365 } catch (final RuntimeException e) { 366 LogUtil.e(TAG, "RuntimeException in CameraManager.selectCameraByIndex", e); 367 if (mListener != null) { 368 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 369 } 370 } 371 } 372 373 @VisibleForTesting 374 CameraInfo getCameraInfo() { 375 if (mCameraIndex == NO_CAMERA_SELECTED) { 376 return null; 377 } 378 return mCameraInfo; 379 } 380 381 /** @return True if this device has camera capabilities */ 382 boolean hasAnyCamera() { 383 return sCameraWrapper.getNumberOfCameras() > 0; 384 } 385 386 /** @return True if the device has both a front and back camera */ 387 boolean hasFrontAndBackCamera() { 388 return mHasFrontAndBackCamera; 389 } 390 391 /** 392 * Opens the camera on a separate thread and initiates the preview if one is available 393 */ 394 void openCamera() { 395 if (mCameraIndex == NO_CAMERA_SELECTED) { 396 // Ensure a selected camera if none is currently selected. This may happen if the 397 // camera chooser is not the default media chooser. 398 selectCamera(CameraInfo.CAMERA_FACING_BACK); 399 } 400 mOpenRequested = true; 401 // We're already opening the camera or already have the camera handle, nothing more to do 402 if (mPendingOpenCameraIndex == mCameraIndex || mCamera != null) { 403 return; 404 } 405 406 // True if the task to open the camera has to be delayed until the current one completes 407 boolean delayTask = false; 408 409 // Cancel any previous open camera tasks 410 if (mOpenCameraTask != null) { 411 mPendingOpenCameraIndex = NO_CAMERA_SELECTED; 412 delayTask = true; 413 } 414 415 mPendingOpenCameraIndex = mCameraIndex; 416 mOpenCameraTask = new AsyncTask<Integer, Void, Camera>() { 417 private Exception mException; 418 419 @Override 420 protected Camera doInBackground(final Integer... params) { 421 try { 422 final int cameraIndex = params[0]; 423 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 424 LogUtil.v(TAG, "Opening camera " + mCameraIndex); 425 } 426 return sCameraWrapper.open(cameraIndex); 427 } catch (final Exception e) { 428 LogUtil.e(TAG, "Exception while opening camera", e); 429 mException = e; 430 return null; 431 } 432 } 433 434 @Override 435 protected void onPostExecute(final Camera camera) { 436 // If we completed, but no longer want this camera, then release the camera 437 if (mOpenCameraTask != this || !mOpenRequested) { 438 releaseCamera(camera); 439 cleanup(); 440 return; 441 } 442 443 cleanup(); 444 445 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 446 LogUtil.v(TAG, "Opened camera " + mCameraIndex + " " + (camera != null)); 447 } 448 449 setCamera(camera); 450 if (camera == null) { 451 if (mListener != null) { 452 mListener.onCameraError(ERROR_OPENING_CAMERA, mException); 453 } 454 LogUtil.e(TAG, "Error opening camera"); 455 } 456 } 457 458 @Override 459 protected void onCancelled() { 460 super.onCancelled(); 461 cleanup(); 462 } 463 464 private void cleanup() { 465 mPendingOpenCameraIndex = NO_CAMERA_SELECTED; 466 if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) { 467 // If there's another task waiting on this one to complete, start it now 468 mOpenCameraTask.execute(mCameraIndex); 469 } else { 470 mOpenCameraTask = null; 471 } 472 473 } 474 }; 475 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 476 LogUtil.v(TAG, "Start opening camera " + mCameraIndex); 477 } 478 479 if (!delayTask) { 480 mOpenCameraTask.execute(mCameraIndex); 481 } 482 } 483 484 boolean isVideoMode() { 485 return mVideoModeRequested; 486 } 487 488 boolean isRecording() { 489 return mVideoModeRequested && mVideoCallback != null; 490 } 491 492 void setVideoMode(final boolean videoMode) { 493 if (mVideoModeRequested == videoMode) { 494 return; 495 } 496 mVideoModeRequested = videoMode; 497 tryInitOrCleanupVideoMode(); 498 } 499 500 /** Closes the camera releasing the resources it uses */ 501 void closeCamera() { 502 mOpenRequested = false; 503 setCamera(null); 504 } 505 506 /** Temporarily closes the camera if it is open */ 507 void onPause() { 508 setCamera(null); 509 } 510 511 /** Reopens the camera if it was opened when onPause was called */ 512 void onResume() { 513 if (mOpenRequested) { 514 openCamera(); 515 } 516 } 517 518 /** 519 * Sets the listener which will be notified of errors or other events in the camera 520 * @param listener The listener to notify 521 */ 522 void setListener(final CameraManagerListener listener) { 523 Assert.isMainThread(); 524 mListener = listener; 525 if (!mIsHardwareAccelerationSupported && mListener != null) { 526 mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); 527 } 528 } 529 530 void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { 531 mSubscriptionDataProvider = provider; 532 } 533 534 void takePicture(final float heightPercent, @NonNull final MediaCallback callback) { 535 Assert.isTrue(!mVideoModeRequested); 536 Assert.isTrue(!mTakingPicture); 537 Assert.notNull(callback); 538 if (mCamera == null) { 539 // The caller should have checked isCameraAvailable first, but just in case, protect 540 // against a null camera by notifying the callback that taking the picture didn't work 541 callback.onMediaFailed(null); 542 return; 543 } 544 final Camera.PictureCallback jpegCallback = new Camera.PictureCallback() { 545 @Override 546 public void onPictureTaken(final byte[] bytes, final Camera camera) { 547 mTakingPicture = false; 548 if (mCamera != camera) { 549 // This may happen if the camera was changed between front/back while the 550 // picture is being taken. 551 callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED); 552 return; 553 } 554 555 if (bytes == null) { 556 callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA); 557 return; 558 } 559 560 final Camera.Size size = camera.getParameters().getPictureSize(); 561 int width; 562 int height; 563 if (mRotation == 90 || mRotation == 270) { 564 width = size.height; 565 height = size.width; 566 } else { 567 width = size.width; 568 height = size.height; 569 } 570 new ImagePersistTask( 571 width, height, heightPercent, bytes, mCameraPreview.getContext(), callback) 572 .executeOnThreadPool(); 573 } 574 }; 575 576 mTakingPicture = true; 577 try { 578 mCamera.takePicture( 579 null /* shutter */, 580 null /* raw */, 581 null /* postView */, 582 jpegCallback); 583 } catch (final RuntimeException e) { 584 LogUtil.e(TAG, "RuntimeException in CameraManager.takePicture", e); 585 mTakingPicture = false; 586 if (mListener != null) { 587 mListener.onCameraError(ERROR_TAKING_PICTURE, e); 588 } 589 } 590 } 591 592 void startVideo(final MediaCallback callback) { 593 Assert.notNull(callback); 594 Assert.isTrue(!isRecording()); 595 mVideoCallback = callback; 596 tryStartVideoCapture(); 597 } 598 599 /** 600 * Asynchronously releases a camera 601 * @param camera The camera to release 602 */ 603 private void releaseCamera(final Camera camera) { 604 if (camera == null) { 605 return; 606 } 607 608 mFocusOverlayManager.onCameraReleased(); 609 610 new AsyncTask<Void, Void, Void>() { 611 @Override 612 protected Void doInBackground(final Void... params) { 613 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 614 LogUtil.v(TAG, "Releasing camera " + mCameraIndex); 615 } 616 sCameraWrapper.release(camera); 617 return null; 618 } 619 }.execute(); 620 } 621 622 private void releaseMediaRecorder(final boolean cleanupFile) { 623 if (mMediaRecorder == null) { 624 return; 625 } 626 mVideoModeRequested = false; 627 628 if (cleanupFile) { 629 mMediaRecorder.cleanupTempFile(); 630 if (mVideoCallback != null) { 631 final MediaCallback callback = mVideoCallback; 632 mVideoCallback = null; 633 // Notify the callback that we've stopped recording 634 callback.onMediaReady(null /*uri*/, null /*contentType*/, 0 /*width*/, 635 0 /*height*/); 636 } 637 } 638 639 mMediaRecorder.release(); 640 mMediaRecorder = null; 641 642 if (mCamera != null) { 643 try { 644 mCamera.reconnect(); 645 } catch (final IOException e) { 646 LogUtil.e(TAG, "IOException in CameraManager.releaseMediaRecorder", e); 647 if (mListener != null) { 648 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 649 } 650 } catch (final RuntimeException e) { 651 LogUtil.e(TAG, "RuntimeException in CameraManager.releaseMediaRecorder", e); 652 if (mListener != null) { 653 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 654 } 655 } 656 } 657 restoreRequestedOrientation(); 658 } 659 660 /** Updates the orientation of the camera to match the orientation of the device */ 661 private void updateCameraOrientation() { 662 if (mCamera == null || mCameraPreview == null || mTakingPicture) { 663 return; 664 } 665 666 final WindowManager windowManager = 667 (WindowManager) mCameraPreview.getContext().getSystemService( 668 Context.WINDOW_SERVICE); 669 670 int degrees = 0; 671 switch (windowManager.getDefaultDisplay().getRotation()) { 672 case Surface.ROTATION_0: degrees = 0; break; 673 case Surface.ROTATION_90: degrees = 90; break; 674 case Surface.ROTATION_180: degrees = 180; break; 675 case Surface.ROTATION_270: degrees = 270; break; 676 } 677 678 // The display orientation of the camera (this controls the preview image). 679 int orientation; 680 681 // The clockwise rotation angle relative to the orientation of the camera. This affects 682 // pictures returned by the camera in Camera.PictureCallback. 683 int rotation; 684 if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 685 orientation = (mCameraInfo.orientation + degrees) % 360; 686 rotation = orientation; 687 // compensate the mirror but only for orientation 688 orientation = (360 - orientation) % 360; 689 } else { // back-facing 690 orientation = (mCameraInfo.orientation - degrees + 360) % 360; 691 rotation = orientation; 692 } 693 mRotation = rotation; 694 if (mMediaRecorder == null) { 695 try { 696 mCamera.setDisplayOrientation(orientation); 697 final Camera.Parameters params = mCamera.getParameters(); 698 params.setRotation(rotation); 699 mCamera.setParameters(params); 700 } catch (final RuntimeException e) { 701 LogUtil.e(TAG, "RuntimeException in CameraManager.updateCameraOrientation", e); 702 if (mListener != null) { 703 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 704 } 705 } 706 } 707 } 708 709 /** Sets the current camera, releasing any previously opened camera */ 710 private void setCamera(final Camera camera) { 711 if (mCamera == camera) { 712 return; 713 } 714 715 releaseMediaRecorder(true /* cleanupFile */); 716 releaseCamera(mCamera); 717 mCamera = camera; 718 tryShowPreview(); 719 if (mListener != null) { 720 mListener.onCameraChanged(); 721 } 722 } 723 724 /** Shows the preview if the camera is open and the preview is loaded */ 725 private void tryShowPreview() { 726 if (mCameraPreview == null || mCamera == null) { 727 if (mOrientationHandler != null) { 728 mOrientationHandler.disable(); 729 mOrientationHandler = null; 730 } 731 releaseMediaRecorder(true /* cleanupFile */); 732 mFocusOverlayManager.onPreviewStopped(); 733 return; 734 } 735 try { 736 mCamera.stopPreview(); 737 updateCameraOrientation(); 738 739 final Camera.Parameters params = mCamera.getParameters(); 740 final Camera.Size pictureSize = chooseBestPictureSize(); 741 final Camera.Size previewSize = chooseBestPreviewSize(pictureSize); 742 params.setPreviewSize(previewSize.width, previewSize.height); 743 params.setPictureSize(pictureSize.width, pictureSize.height); 744 logCameraSize("Setting preview size: ", previewSize); 745 logCameraSize("Setting picture size: ", pictureSize); 746 mCameraPreview.setSize(previewSize, mCameraInfo.orientation); 747 for (final String focusMode : params.getSupportedFocusModes()) { 748 if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { 749 // Use continuous focus if available 750 params.setFocusMode(focusMode); 751 break; 752 } 753 } 754 755 mCamera.setParameters(params); 756 mCameraPreview.startPreview(mCamera); 757 mCamera.startPreview(); 758 mCamera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() { 759 @Override 760 public void onAutoFocusMoving(final boolean start, final Camera camera) { 761 mFocusOverlayManager.onAutoFocusMoving(start); 762 } 763 }); 764 mFocusOverlayManager.setParameters(mCamera.getParameters()); 765 mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK); 766 mFocusOverlayManager.onPreviewStarted(); 767 tryInitOrCleanupVideoMode(); 768 if (mOrientationHandler == null) { 769 mOrientationHandler = new OrientationHandler(mCameraPreview.getContext()); 770 mOrientationHandler.enable(); 771 } 772 } catch (final IOException e) { 773 LogUtil.e(TAG, "IOException in CameraManager.tryShowPreview", e); 774 if (mListener != null) { 775 mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); 776 } 777 } catch (final RuntimeException e) { 778 LogUtil.e(TAG, "RuntimeException in CameraManager.tryShowPreview", e); 779 if (mListener != null) { 780 mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); 781 } 782 } 783 } 784 785 private void tryInitOrCleanupVideoMode() { 786 if (!mVideoModeRequested || mCamera == null || mCameraPreview == null) { 787 releaseMediaRecorder(true /* cleanupFile */); 788 return; 789 } 790 791 if (mMediaRecorder != null) { 792 return; 793 } 794 795 try { 796 mCamera.unlock(); 797 final int maxMessageSize = getMmsConfig().getMaxMessageSize(); 798 mMediaRecorder = new MmsVideoRecorder(mCamera, mCameraIndex, mRotation, maxMessageSize); 799 mMediaRecorder.prepare(); 800 } catch (final FileNotFoundException e) { 801 LogUtil.e(TAG, "FileNotFoundException in CameraManager.tryInitOrCleanupVideoMode", e); 802 if (mListener != null) { 803 mListener.onCameraError(ERROR_STORAGE_FAILURE, e); 804 } 805 setVideoMode(false); 806 return; 807 } catch (final IOException e) { 808 LogUtil.e(TAG, "IOException in CameraManager.tryInitOrCleanupVideoMode", e); 809 if (mListener != null) { 810 mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); 811 } 812 setVideoMode(false); 813 return; 814 } catch (final RuntimeException e) { 815 LogUtil.e(TAG, "RuntimeException in CameraManager.tryInitOrCleanupVideoMode", e); 816 if (mListener != null) { 817 mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); 818 } 819 setVideoMode(false); 820 return; 821 } 822 823 tryStartVideoCapture(); 824 } 825 826 private void tryStartVideoCapture() { 827 if (mMediaRecorder == null || mVideoCallback == null) { 828 return; 829 } 830 831 mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() { 832 @Override 833 public void onError(final MediaRecorder mediaRecorder, final int what, 834 final int extra) { 835 if (mListener != null) { 836 mListener.onCameraError(ERROR_RECORDING_VIDEO, null); 837 } 838 restoreRequestedOrientation(); 839 } 840 }); 841 842 mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { 843 @Override 844 public void onInfo(final MediaRecorder mediaRecorder, final int what, final int extra) { 845 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || 846 what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 847 stopVideo(); 848 } 849 } 850 }); 851 852 try { 853 mMediaRecorder.start(); 854 final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); 855 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 856 lockOrientation(); 857 } catch (final IllegalStateException e) { 858 LogUtil.e(TAG, "IllegalStateException in CameraManager.tryStartVideoCapture", e); 859 if (mListener != null) { 860 mListener.onCameraError(ERROR_RECORDING_VIDEO, e); 861 } 862 setVideoMode(false); 863 restoreRequestedOrientation(); 864 } catch (final RuntimeException e) { 865 LogUtil.e(TAG, "RuntimeException in CameraManager.tryStartVideoCapture", e); 866 if (mListener != null) { 867 mListener.onCameraError(ERROR_RECORDING_VIDEO, e); 868 } 869 setVideoMode(false); 870 restoreRequestedOrientation(); 871 } 872 } 873 874 void stopVideo() { 875 int width = ImageRequest.UNSPECIFIED_SIZE; 876 int height = ImageRequest.UNSPECIFIED_SIZE; 877 Uri uri = null; 878 String contentType = null; 879 try { 880 final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); 881 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 882 mMediaRecorder.stop(); 883 width = mMediaRecorder.getVideoWidth(); 884 height = mMediaRecorder.getVideoHeight(); 885 uri = mMediaRecorder.getVideoUri(); 886 contentType = mMediaRecorder.getContentType(); 887 } catch (final RuntimeException e) { 888 // MediaRecorder.stop will throw a RuntimeException if the video was too short, let the 889 // finally clause call the callback with null uri and handle cleanup 890 LogUtil.e(TAG, "RuntimeException in CameraManager.stopVideo", e); 891 } finally { 892 final MediaCallback videoCallback = mVideoCallback; 893 mVideoCallback = null; 894 releaseMediaRecorder(false /* cleanupFile */); 895 if (uri == null) { 896 tryInitOrCleanupVideoMode(); 897 } 898 videoCallback.onMediaReady(uri, contentType, width, height); 899 } 900 } 901 902 boolean isCameraAvailable() { 903 return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported; 904 } 905 906 /** 907 * External components call into this to report if hardware acceleration is supported. When 908 * hardware acceleration isn't supported, we need to report an error through the listener 909 * interface 910 * @param isHardwareAccelerationSupported True if the preview is rendering in a hardware 911 * accelerated view. 912 */ 913 void reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported) { 914 Assert.isMainThread(); 915 if (mIsHardwareAccelerationSupported == isHardwareAccelerationSupported) { 916 // If the value hasn't changed nothing more to do 917 return; 918 } 919 920 mIsHardwareAccelerationSupported = isHardwareAccelerationSupported; 921 if (!isHardwareAccelerationSupported) { 922 LogUtil.e(TAG, "Software rendering - cannot open camera"); 923 if (mListener != null) { 924 mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); 925 } 926 } 927 } 928 929 /** Returns the scale factor to scale the width/height to max allowed in MmsConfig */ 930 private float getScaleFactorForMaxAllowedSize(final int width, final int height, 931 final int maxWidth, final int maxHeight) { 932 if (maxWidth <= 0 || maxHeight <= 0) { 933 // MmsConfig initialization runs asynchronously on application startup, so there's a 934 // chance (albeit a very slight one) that we don't have it yet. 935 LogUtil.w(LogUtil.BUGLE_TAG, "Max image size not loaded in MmsConfig"); 936 return 1.0f; 937 } 938 939 if (width <= maxWidth && height <= maxHeight) { 940 // Already meeting requirements. 941 return 1.0f; 942 } 943 944 return Math.min(maxWidth * 1.0f / width, maxHeight * 1.0f / height); 945 } 946 947 private MmsConfig getMmsConfig() { 948 final int subId = mSubscriptionDataProvider != null ? 949 mSubscriptionDataProvider.getConversationSelfSubId() : 950 ParticipantData.DEFAULT_SELF_SUB_ID; 951 return MmsConfig.get(subId); 952 } 953 954 /** 955 * Choose the best picture size by trying to find a size close to the MmsConfig's max size, 956 * which is closest to the screen aspect ratio 957 */ 958 private Camera.Size chooseBestPictureSize() { 959 final Context context = mCameraPreview.getContext(); 960 final Resources resources = context.getResources(); 961 final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); 962 final int displayOrientation = resources.getConfiguration().orientation; 963 int cameraOrientation = mCameraInfo.orientation; 964 965 int screenWidth; 966 int screenHeight; 967 if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) { 968 // Rotate the camera orientation 90 degrees to compensate for the rotated display 969 // metrics. Direction doesn't matter because we're just using it for width/height 970 cameraOrientation += 90; 971 } 972 973 // Check the camera orientation relative to the display. 974 // For 0, 180, 360, the screen width/height are the display width/height 975 // For 90, 270, the screen width/height are inverted from the display 976 if (cameraOrientation % 180 == 0) { 977 screenWidth = displayMetrics.widthPixels; 978 screenHeight = displayMetrics.heightPixels; 979 } else { 980 screenWidth = displayMetrics.heightPixels; 981 screenHeight = displayMetrics.widthPixels; 982 } 983 984 final MmsConfig mmsConfig = getMmsConfig(); 985 final int maxWidth = mmsConfig.getMaxImageWidth(); 986 final int maxHeight = mmsConfig.getMaxImageHeight(); 987 988 // Constrain the size within the max width/height defined by MmsConfig. 989 final float scaleFactor = getScaleFactorForMaxAllowedSize(screenWidth, screenHeight, 990 maxWidth, maxHeight); 991 screenWidth *= scaleFactor; 992 screenHeight *= scaleFactor; 993 994 final float aspectRatio = BugleGservices.get().getFloat( 995 BugleGservicesKeys.CAMERA_ASPECT_RATIO, 996 screenWidth / (float) screenHeight); 997 final List<Camera.Size> sizes = new ArrayList<Camera.Size>( 998 mCamera.getParameters().getSupportedPictureSizes()); 999 final int maxPixels = maxWidth * maxHeight; 1000 1001 // Sort the sizes so the best size is first 1002 Collections.sort(sizes, new SizeComparator(maxWidth, maxHeight, aspectRatio, maxPixels)); 1003 1004 return sizes.get(0); 1005 } 1006 1007 /** 1008 * Chose the best preview size based on the picture size. Try to find a size with the same 1009 * aspect ratio and size as the picture if possible 1010 */ 1011 private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) { 1012 final List<Camera.Size> sizes = new ArrayList<Camera.Size>( 1013 mCamera.getParameters().getSupportedPreviewSizes()); 1014 final float aspectRatio = pictureSize.width / (float) pictureSize.height; 1015 final int capturePixels = pictureSize.width * pictureSize.height; 1016 1017 // Sort the sizes so the best size is first 1018 Collections.sort(sizes, new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, 1019 aspectRatio, capturePixels)); 1020 1021 return sizes.get(0); 1022 } 1023 1024 private class OrientationHandler extends OrientationEventListener { 1025 OrientationHandler(final Context context) { 1026 super(context); 1027 } 1028 1029 @Override 1030 public void onOrientationChanged(final int orientation) { 1031 updateCameraOrientation(); 1032 } 1033 } 1034 1035 private static class SizeComparator implements Comparator<Camera.Size> { 1036 private static final int PREFER_LEFT = -1; 1037 private static final int PREFER_RIGHT = 1; 1038 1039 // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit 1040 private final int mMaxWidth; 1041 private final int mMaxHeight; 1042 1043 // The desired aspect ratio 1044 private final float mTargetAspectRatio; 1045 1046 // The desired size (width x height) to try to match 1047 private final int mTargetPixels; 1048 1049 public SizeComparator(final int maxWidth, final int maxHeight, 1050 final float targetAspectRatio, final int targetPixels) { 1051 mMaxWidth = maxWidth; 1052 mMaxHeight = maxHeight; 1053 mTargetAspectRatio = targetAspectRatio; 1054 mTargetPixels = targetPixels; 1055 } 1056 1057 /** 1058 * Returns a negative value if left is a better choice than right, or a positive value if 1059 * right is a better choice is better than left. 0 if they are equal 1060 */ 1061 @Override 1062 public int compare(final Camera.Size left, final Camera.Size right) { 1063 // If one size is less than the max size prefer it over the other 1064 if ((left.width <= mMaxWidth && left.height <= mMaxHeight) != 1065 (right.width <= mMaxWidth && right.height <= mMaxHeight)) { 1066 return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT; 1067 } 1068 1069 // If one is closer to the target aspect ratio, prefer it. 1070 final float leftAspectRatio = left.width / (float) left.height; 1071 final float rightAspectRatio = right.width / (float) right.height; 1072 final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio); 1073 final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio); 1074 if (leftAspectRatioDiff != rightAspectRatioDiff) { 1075 return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? 1076 PREFER_LEFT : PREFER_RIGHT; 1077 } 1078 1079 // At this point they have the same aspect ratio diff and are either both bigger 1080 // than the max size or both smaller than the max size, so prefer the one closest 1081 // to target size 1082 final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels); 1083 final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels); 1084 return leftDiff - rightDiff; 1085 } 1086 } 1087 1088 @Override // From FocusOverlayManager.Listener 1089 public void autoFocus() { 1090 if (mCamera == null) { 1091 return; 1092 } 1093 1094 try { 1095 mCamera.autoFocus(new Camera.AutoFocusCallback() { 1096 @Override 1097 public void onAutoFocus(final boolean success, final Camera camera) { 1098 mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */); 1099 } 1100 }); 1101 } catch (final RuntimeException e) { 1102 LogUtil.e(TAG, "RuntimeException in CameraManager.autoFocus", e); 1103 // If autofocus fails, the camera should have called the callback with success=false, 1104 // but some throw an exception here 1105 mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/); 1106 } 1107 } 1108 1109 @Override // From FocusOverlayManager.Listener 1110 public void cancelAutoFocus() { 1111 if (mCamera == null) { 1112 return; 1113 } 1114 try { 1115 mCamera.cancelAutoFocus(); 1116 } catch (final RuntimeException e) { 1117 // Ignore 1118 LogUtil.e(TAG, "RuntimeException in CameraManager.cancelAutoFocus", e); 1119 } 1120 } 1121 1122 @Override // From FocusOverlayManager.Listener 1123 public boolean capture() { 1124 return false; 1125 } 1126 1127 @Override // From FocusOverlayManager.Listener 1128 public void setFocusParameters() { 1129 if (mCamera == null) { 1130 return; 1131 } 1132 try { 1133 final Camera.Parameters parameters = mCamera.getParameters(); 1134 parameters.setFocusMode(mFocusOverlayManager.getFocusMode()); 1135 if (parameters.getMaxNumFocusAreas() > 0) { 1136 // Don't set focus areas (even to null) if focus areas aren't supported, camera may 1137 // crash 1138 parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas()); 1139 } 1140 parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas()); 1141 mCamera.setParameters(parameters); 1142 } catch (final RuntimeException e) { 1143 // This occurs when the device is out of space or when the camera is locked 1144 LogUtil.e(TAG, "RuntimeException in CameraManager setFocusParameters"); 1145 } 1146 } 1147 1148 private void logCameraSize(final String prefix, final Camera.Size size) { 1149 // Log the camera size and aspect ratio for help when examining bug reports for camera 1150 // failures 1151 LogUtil.i(TAG, prefix + size.width + "x" + size.height + 1152 " (" + (size.width / (float) size.height) + ")"); 1153 } 1154 1155 1156 private Integer mSavedOrientation = null; 1157 1158 private void lockOrientation() { 1159 // when we start recording, lock our orientation 1160 final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); 1161 final WindowManager windowManager = 1162 (WindowManager) a.getSystemService(Context.WINDOW_SERVICE); 1163 final int rotation = windowManager.getDefaultDisplay().getRotation(); 1164 1165 mSavedOrientation = a.getRequestedOrientation(); 1166 switch (rotation) { 1167 case Surface.ROTATION_0: 1168 a.setRequestedOrientation( 1169 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 1170 break; 1171 case Surface.ROTATION_90: 1172 a.setRequestedOrientation( 1173 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 1174 break; 1175 case Surface.ROTATION_180: 1176 a.setRequestedOrientation( 1177 ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); 1178 break; 1179 case Surface.ROTATION_270: 1180 a.setRequestedOrientation( 1181 ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); 1182 break; 1183 } 1184 1185 } 1186 1187 private void restoreRequestedOrientation() { 1188 if (mSavedOrientation != null) { 1189 final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); 1190 if (a != null) { 1191 a.setRequestedOrientation(mSavedOrientation); 1192 } 1193 mSavedOrientation = null; 1194 } 1195 } 1196 1197 static boolean hasCameraPermission() { 1198 return OsUtil.hasPermission(Manifest.permission.CAMERA); 1199 } 1200 } 1201