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 androidx.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 { getNumberOfCameras()83 int getNumberOfCameras(); getCameraInfo(int index, CameraInfo cameraInfo)84 void getCameraInfo(int index, CameraInfo cameraInfo); open(int cameraId)85 Camera open(int cameraId); 86 /** Add a wrapper for release because a final method cannot be mocked */ release(Camera camera)87 void release(Camera camera); 88 } 89 90 /** 91 * Callbacks for the camera manager listener 92 */ 93 interface CameraManagerListener { onCameraError(int errorCode, Exception e)94 void onCameraError(int errorCode, Exception e); onCameraChanged()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 onMediaReady(Uri uriToMedia, String contentType, int width, int height)105 void onMediaReady(Uri uriToMedia, String contentType, int width, int height); onMediaFailed(Exception exception)106 void onMediaFailed(Exception exception); onMediaInfo(int what)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 CameraManager()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 */ get()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 setCameraWrapper(final CameraWrapper cameraWrapper)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 */ setSurface(final CameraPreview preview)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 setRenderOverlay(final RenderOverlay renderOverlay)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 */ swapCamera()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 */ selectCamera(final int desiredFacing)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 getCameraIndex()350 int getCameraIndex() { 351 return mCameraIndex; 352 } 353 selectCameraByIndex(final int cameraIndex)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 getCameraInfo()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 */ hasAnyCamera()382 boolean hasAnyCamera() { 383 return sCameraWrapper.getNumberOfCameras() > 0; 384 } 385 386 /** @return True if the device has both a front and back camera */ hasFrontAndBackCamera()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 */ openCamera()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 isVideoMode()484 boolean isVideoMode() { 485 return mVideoModeRequested; 486 } 487 isRecording()488 boolean isRecording() { 489 return mVideoModeRequested && mVideoCallback != null; 490 } 491 setVideoMode(final boolean videoMode)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 */ closeCamera()501 void closeCamera() { 502 mOpenRequested = false; 503 setCamera(null); 504 } 505 506 /** Temporarily closes the camera if it is open */ onPause()507 void onPause() { 508 setCamera(null); 509 } 510 511 /** Reopens the camera if it was opened when onPause was called */ onResume()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 */ setListener(final CameraManagerListener listener)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 setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider)530 void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { 531 mSubscriptionDataProvider = provider; 532 } 533 takePicture(final float heightPercent, @NonNull final MediaCallback callback)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 startVideo(final MediaCallback callback)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 */ releaseCamera(final Camera camera)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 releaseMediaRecorder(final boolean cleanupFile)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.closeVideoFileDescriptor(); 640 mMediaRecorder.release(); 641 mMediaRecorder = null; 642 643 if (mCamera != null) { 644 try { 645 mCamera.reconnect(); 646 } catch (final IOException e) { 647 LogUtil.e(TAG, "IOException in CameraManager.releaseMediaRecorder", e); 648 if (mListener != null) { 649 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 650 } 651 } catch (final RuntimeException e) { 652 LogUtil.e(TAG, "RuntimeException in CameraManager.releaseMediaRecorder", e); 653 if (mListener != null) { 654 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 655 } 656 } 657 } 658 restoreRequestedOrientation(); 659 } 660 661 /** Updates the orientation of the camera to match the orientation of the device */ updateCameraOrientation()662 private void updateCameraOrientation() { 663 if (mCamera == null || mCameraPreview == null || mTakingPicture) { 664 return; 665 } 666 667 final WindowManager windowManager = 668 (WindowManager) mCameraPreview.getContext().getSystemService( 669 Context.WINDOW_SERVICE); 670 671 int degrees = 0; 672 switch (windowManager.getDefaultDisplay().getRotation()) { 673 case Surface.ROTATION_0: degrees = 0; break; 674 case Surface.ROTATION_90: degrees = 90; break; 675 case Surface.ROTATION_180: degrees = 180; break; 676 case Surface.ROTATION_270: degrees = 270; break; 677 } 678 679 // The display orientation of the camera (this controls the preview image). 680 int orientation; 681 682 // The clockwise rotation angle relative to the orientation of the camera. This affects 683 // pictures returned by the camera in Camera.PictureCallback. 684 int rotation; 685 if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 686 orientation = (mCameraInfo.orientation + degrees) % 360; 687 rotation = orientation; 688 // compensate the mirror but only for orientation 689 orientation = (360 - orientation) % 360; 690 } else { // back-facing 691 orientation = (mCameraInfo.orientation - degrees + 360) % 360; 692 rotation = orientation; 693 } 694 mRotation = rotation; 695 if (mMediaRecorder == null) { 696 try { 697 mCamera.setDisplayOrientation(orientation); 698 final Camera.Parameters params = mCamera.getParameters(); 699 params.setRotation(rotation); 700 mCamera.setParameters(params); 701 } catch (final RuntimeException e) { 702 LogUtil.e(TAG, "RuntimeException in CameraManager.updateCameraOrientation", e); 703 if (mListener != null) { 704 mListener.onCameraError(ERROR_OPENING_CAMERA, e); 705 } 706 } 707 } 708 } 709 710 /** Sets the current camera, releasing any previously opened camera */ setCamera(final Camera camera)711 private void setCamera(final Camera camera) { 712 if (mCamera == camera) { 713 return; 714 } 715 716 releaseMediaRecorder(true /* cleanupFile */); 717 releaseCamera(mCamera); 718 mCamera = camera; 719 tryShowPreview(); 720 if (mListener != null) { 721 mListener.onCameraChanged(); 722 } 723 } 724 725 /** Shows the preview if the camera is open and the preview is loaded */ tryShowPreview()726 private void tryShowPreview() { 727 if (mCameraPreview == null || mCamera == null) { 728 if (mOrientationHandler != null) { 729 mOrientationHandler.disable(); 730 mOrientationHandler = null; 731 } 732 releaseMediaRecorder(true /* cleanupFile */); 733 mFocusOverlayManager.onPreviewStopped(); 734 return; 735 } 736 try { 737 mCamera.stopPreview(); 738 updateCameraOrientation(); 739 740 final Camera.Parameters params = mCamera.getParameters(); 741 final Camera.Size pictureSize = chooseBestPictureSize(); 742 final Camera.Size previewSize = chooseBestPreviewSize(pictureSize); 743 params.setPreviewSize(previewSize.width, previewSize.height); 744 params.setPictureSize(pictureSize.width, pictureSize.height); 745 logCameraSize("Setting preview size: ", previewSize); 746 logCameraSize("Setting picture size: ", pictureSize); 747 mCameraPreview.setSize(previewSize, mCameraInfo.orientation); 748 for (final String focusMode : params.getSupportedFocusModes()) { 749 if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { 750 // Use continuous focus if available 751 params.setFocusMode(focusMode); 752 break; 753 } 754 } 755 756 mCamera.setParameters(params); 757 mCameraPreview.startPreview(mCamera); 758 mCamera.startPreview(); 759 mCamera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() { 760 @Override 761 public void onAutoFocusMoving(final boolean start, final Camera camera) { 762 mFocusOverlayManager.onAutoFocusMoving(start); 763 } 764 }); 765 mFocusOverlayManager.setParameters(mCamera.getParameters()); 766 mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK); 767 mFocusOverlayManager.onPreviewStarted(); 768 tryInitOrCleanupVideoMode(); 769 if (mOrientationHandler == null) { 770 mOrientationHandler = new OrientationHandler(mCameraPreview.getContext()); 771 mOrientationHandler.enable(); 772 } 773 } catch (final IOException e) { 774 LogUtil.e(TAG, "IOException in CameraManager.tryShowPreview", e); 775 if (mListener != null) { 776 mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); 777 } 778 } catch (final RuntimeException e) { 779 LogUtil.e(TAG, "RuntimeException in CameraManager.tryShowPreview", e); 780 if (mListener != null) { 781 mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); 782 } 783 } 784 } 785 tryInitOrCleanupVideoMode()786 private void tryInitOrCleanupVideoMode() { 787 if (!mVideoModeRequested || mCamera == null || mCameraPreview == null) { 788 releaseMediaRecorder(true /* cleanupFile */); 789 return; 790 } 791 792 if (mMediaRecorder != null) { 793 return; 794 } 795 796 try { 797 mCamera.unlock(); 798 final int maxMessageSize = getMmsConfig().getMaxMessageSize(); 799 mMediaRecorder = new MmsVideoRecorder(mCamera, mCameraIndex, mRotation, maxMessageSize); 800 mMediaRecorder.prepare(); 801 } catch (final FileNotFoundException e) { 802 LogUtil.e(TAG, "FileNotFoundException in CameraManager.tryInitOrCleanupVideoMode", e); 803 if (mListener != null) { 804 mListener.onCameraError(ERROR_STORAGE_FAILURE, e); 805 } 806 setVideoMode(false); 807 return; 808 } catch (final IOException e) { 809 LogUtil.e(TAG, "IOException in CameraManager.tryInitOrCleanupVideoMode", e); 810 if (mListener != null) { 811 mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); 812 } 813 setVideoMode(false); 814 return; 815 } catch (final RuntimeException e) { 816 LogUtil.e(TAG, "RuntimeException in CameraManager.tryInitOrCleanupVideoMode", e); 817 if (mListener != null) { 818 mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); 819 } 820 setVideoMode(false); 821 return; 822 } 823 824 tryStartVideoCapture(); 825 } 826 tryStartVideoCapture()827 private void tryStartVideoCapture() { 828 if (mMediaRecorder == null || mVideoCallback == null) { 829 return; 830 } 831 832 mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() { 833 @Override 834 public void onError(final MediaRecorder mediaRecorder, final int what, 835 final int extra) { 836 if (mListener != null) { 837 mListener.onCameraError(ERROR_RECORDING_VIDEO, null); 838 } 839 restoreRequestedOrientation(); 840 } 841 }); 842 843 mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { 844 @Override 845 public void onInfo(final MediaRecorder mediaRecorder, final int what, final int extra) { 846 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || 847 what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 848 stopVideo(); 849 } 850 } 851 }); 852 853 try { 854 mMediaRecorder.start(); 855 final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); 856 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 857 lockOrientation(); 858 } catch (final IllegalStateException e) { 859 LogUtil.e(TAG, "IllegalStateException in CameraManager.tryStartVideoCapture", e); 860 if (mListener != null) { 861 mListener.onCameraError(ERROR_RECORDING_VIDEO, e); 862 } 863 setVideoMode(false); 864 restoreRequestedOrientation(); 865 } catch (final RuntimeException e) { 866 LogUtil.e(TAG, "RuntimeException in CameraManager.tryStartVideoCapture", e); 867 if (mListener != null) { 868 mListener.onCameraError(ERROR_RECORDING_VIDEO, e); 869 } 870 setVideoMode(false); 871 restoreRequestedOrientation(); 872 } 873 } 874 stopVideo()875 void stopVideo() { 876 int width = ImageRequest.UNSPECIFIED_SIZE; 877 int height = ImageRequest.UNSPECIFIED_SIZE; 878 Uri uri = null; 879 String contentType = null; 880 try { 881 final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); 882 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 883 mMediaRecorder.stop(); 884 width = mMediaRecorder.getVideoWidth(); 885 height = mMediaRecorder.getVideoHeight(); 886 uri = mMediaRecorder.getVideoUri(); 887 contentType = mMediaRecorder.getContentType(); 888 } catch (final RuntimeException e) { 889 // MediaRecorder.stop will throw a RuntimeException if the video was too short, let the 890 // finally clause call the callback with null uri and handle cleanup 891 LogUtil.e(TAG, "RuntimeException in CameraManager.stopVideo", e); 892 } finally { 893 final MediaCallback videoCallback = mVideoCallback; 894 mVideoCallback = null; 895 releaseMediaRecorder(false /* cleanupFile */); 896 if (uri == null) { 897 tryInitOrCleanupVideoMode(); 898 } 899 videoCallback.onMediaReady(uri, contentType, width, height); 900 } 901 } 902 isCameraAvailable()903 boolean isCameraAvailable() { 904 return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported; 905 } 906 907 /** 908 * External components call into this to report if hardware acceleration is supported. When 909 * hardware acceleration isn't supported, we need to report an error through the listener 910 * interface 911 * @param isHardwareAccelerationSupported True if the preview is rendering in a hardware 912 * accelerated view. 913 */ reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported)914 void reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported) { 915 Assert.isMainThread(); 916 if (mIsHardwareAccelerationSupported == isHardwareAccelerationSupported) { 917 // If the value hasn't changed nothing more to do 918 return; 919 } 920 921 mIsHardwareAccelerationSupported = isHardwareAccelerationSupported; 922 if (!isHardwareAccelerationSupported) { 923 LogUtil.e(TAG, "Software rendering - cannot open camera"); 924 if (mListener != null) { 925 mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); 926 } 927 } 928 } 929 930 /** Returns the scale factor to scale the width/height to max allowed in MmsConfig */ getScaleFactorForMaxAllowedSize(final int width, final int height, final int maxWidth, final int maxHeight)931 private float getScaleFactorForMaxAllowedSize(final int width, final int height, 932 final int maxWidth, final int maxHeight) { 933 if (maxWidth <= 0 || maxHeight <= 0) { 934 // MmsConfig initialization runs asynchronously on application startup, so there's a 935 // chance (albeit a very slight one) that we don't have it yet. 936 LogUtil.w(LogUtil.BUGLE_TAG, "Max image size not loaded in MmsConfig"); 937 return 1.0f; 938 } 939 940 if (width <= maxWidth && height <= maxHeight) { 941 // Already meeting requirements. 942 return 1.0f; 943 } 944 945 return Math.min(maxWidth * 1.0f / width, maxHeight * 1.0f / height); 946 } 947 getMmsConfig()948 private MmsConfig getMmsConfig() { 949 final int subId = mSubscriptionDataProvider != null ? 950 mSubscriptionDataProvider.getConversationSelfSubId() : 951 ParticipantData.DEFAULT_SELF_SUB_ID; 952 return MmsConfig.get(subId); 953 } 954 955 /** 956 * Choose the best picture size by trying to find a size close to the MmsConfig's max size, 957 * which is closest to the screen aspect ratio 958 */ chooseBestPictureSize()959 private Camera.Size chooseBestPictureSize() { 960 final Context context = mCameraPreview.getContext(); 961 final Resources resources = context.getResources(); 962 final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); 963 final int displayOrientation = resources.getConfiguration().orientation; 964 int cameraOrientation = mCameraInfo.orientation; 965 966 int screenWidth; 967 int screenHeight; 968 if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) { 969 // Rotate the camera orientation 90 degrees to compensate for the rotated display 970 // metrics. Direction doesn't matter because we're just using it for width/height 971 cameraOrientation += 90; 972 } 973 974 // Check the camera orientation relative to the display. 975 // For 0, 180, 360, the screen width/height are the display width/height 976 // For 90, 270, the screen width/height are inverted from the display 977 if (cameraOrientation % 180 == 0) { 978 screenWidth = displayMetrics.widthPixels; 979 screenHeight = displayMetrics.heightPixels; 980 } else { 981 screenWidth = displayMetrics.heightPixels; 982 screenHeight = displayMetrics.widthPixels; 983 } 984 985 final MmsConfig mmsConfig = getMmsConfig(); 986 final int maxWidth = mmsConfig.getMaxImageWidth(); 987 final int maxHeight = mmsConfig.getMaxImageHeight(); 988 989 // Constrain the size within the max width/height defined by MmsConfig. 990 final float scaleFactor = getScaleFactorForMaxAllowedSize(screenWidth, screenHeight, 991 maxWidth, maxHeight); 992 screenWidth *= scaleFactor; 993 screenHeight *= scaleFactor; 994 995 final float aspectRatio = BugleGservices.get().getFloat( 996 BugleGservicesKeys.CAMERA_ASPECT_RATIO, 997 screenWidth / (float) screenHeight); 998 final List<Camera.Size> sizes = new ArrayList<Camera.Size>( 999 mCamera.getParameters().getSupportedPictureSizes()); 1000 final int maxPixels = maxWidth * maxHeight; 1001 1002 // Sort the sizes so the best size is first 1003 Collections.sort(sizes, new SizeComparator(maxWidth, maxHeight, aspectRatio, maxPixels)); 1004 1005 return sizes.get(0); 1006 } 1007 1008 /** 1009 * Chose the best preview size based on the picture size. Try to find a size with the same 1010 * aspect ratio and size as the picture if possible 1011 */ chooseBestPreviewSize(final Camera.Size pictureSize)1012 private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) { 1013 final List<Camera.Size> sizes = new ArrayList<Camera.Size>( 1014 mCamera.getParameters().getSupportedPreviewSizes()); 1015 final float aspectRatio = pictureSize.width / (float) pictureSize.height; 1016 final int capturePixels = pictureSize.width * pictureSize.height; 1017 1018 // Sort the sizes so the best size is first 1019 Collections.sort(sizes, new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, 1020 aspectRatio, capturePixels)); 1021 1022 return sizes.get(0); 1023 } 1024 1025 private class OrientationHandler extends OrientationEventListener { OrientationHandler(final Context context)1026 OrientationHandler(final Context context) { 1027 super(context); 1028 } 1029 1030 @Override onOrientationChanged(final int orientation)1031 public void onOrientationChanged(final int orientation) { 1032 updateCameraOrientation(); 1033 } 1034 } 1035 1036 private static class SizeComparator implements Comparator<Camera.Size> { 1037 private static final int PREFER_LEFT = -1; 1038 private static final int PREFER_RIGHT = 1; 1039 1040 // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit 1041 private final int mMaxWidth; 1042 private final int mMaxHeight; 1043 1044 // The desired aspect ratio 1045 private final float mTargetAspectRatio; 1046 1047 // The desired size (width x height) to try to match 1048 private final int mTargetPixels; 1049 SizeComparator(final int maxWidth, final int maxHeight, final float targetAspectRatio, final int targetPixels)1050 public SizeComparator(final int maxWidth, final int maxHeight, 1051 final float targetAspectRatio, final int targetPixels) { 1052 mMaxWidth = maxWidth; 1053 mMaxHeight = maxHeight; 1054 mTargetAspectRatio = targetAspectRatio; 1055 mTargetPixels = targetPixels; 1056 } 1057 1058 /** 1059 * Returns a negative value if left is a better choice than right, or a positive value if 1060 * right is a better choice is better than left. 0 if they are equal 1061 */ 1062 @Override compare(final Camera.Size left, final Camera.Size right)1063 public int compare(final Camera.Size left, final Camera.Size right) { 1064 // If one size is less than the max size prefer it over the other 1065 if ((left.width <= mMaxWidth && left.height <= mMaxHeight) != 1066 (right.width <= mMaxWidth && right.height <= mMaxHeight)) { 1067 return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT; 1068 } 1069 1070 // If one is closer to the target aspect ratio, prefer it. 1071 final float leftAspectRatio = left.width / (float) left.height; 1072 final float rightAspectRatio = right.width / (float) right.height; 1073 final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio); 1074 final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio); 1075 if (leftAspectRatioDiff != rightAspectRatioDiff) { 1076 return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? 1077 PREFER_LEFT : PREFER_RIGHT; 1078 } 1079 1080 // At this point they have the same aspect ratio diff and are either both bigger 1081 // than the max size or both smaller than the max size, so prefer the one closest 1082 // to target size 1083 final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels); 1084 final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels); 1085 return leftDiff - rightDiff; 1086 } 1087 } 1088 1089 @Override // From FocusOverlayManager.Listener autoFocus()1090 public void autoFocus() { 1091 if (mCamera == null) { 1092 return; 1093 } 1094 1095 try { 1096 mCamera.autoFocus(new Camera.AutoFocusCallback() { 1097 @Override 1098 public void onAutoFocus(final boolean success, final Camera camera) { 1099 mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */); 1100 } 1101 }); 1102 } catch (final RuntimeException e) { 1103 LogUtil.e(TAG, "RuntimeException in CameraManager.autoFocus", e); 1104 // If autofocus fails, the camera should have called the callback with success=false, 1105 // but some throw an exception here 1106 mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/); 1107 } 1108 } 1109 1110 @Override // From FocusOverlayManager.Listener cancelAutoFocus()1111 public void cancelAutoFocus() { 1112 if (mCamera == null) { 1113 return; 1114 } 1115 try { 1116 mCamera.cancelAutoFocus(); 1117 } catch (final RuntimeException e) { 1118 // Ignore 1119 LogUtil.e(TAG, "RuntimeException in CameraManager.cancelAutoFocus", e); 1120 } 1121 } 1122 1123 @Override // From FocusOverlayManager.Listener capture()1124 public boolean capture() { 1125 return false; 1126 } 1127 1128 @Override // From FocusOverlayManager.Listener setFocusParameters()1129 public void setFocusParameters() { 1130 if (mCamera == null) { 1131 return; 1132 } 1133 try { 1134 final Camera.Parameters parameters = mCamera.getParameters(); 1135 parameters.setFocusMode(mFocusOverlayManager.getFocusMode()); 1136 if (parameters.getMaxNumFocusAreas() > 0) { 1137 // Don't set focus areas (even to null) if focus areas aren't supported, camera may 1138 // crash 1139 parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas()); 1140 } 1141 parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas()); 1142 mCamera.setParameters(parameters); 1143 } catch (final RuntimeException e) { 1144 // This occurs when the device is out of space or when the camera is locked 1145 LogUtil.e(TAG, "RuntimeException in CameraManager setFocusParameters"); 1146 } 1147 } 1148 logCameraSize(final String prefix, final Camera.Size size)1149 private void logCameraSize(final String prefix, final Camera.Size size) { 1150 // Log the camera size and aspect ratio for help when examining bug reports for camera 1151 // failures 1152 LogUtil.i(TAG, prefix + size.width + "x" + size.height + 1153 " (" + (size.width / (float) size.height) + ")"); 1154 } 1155 1156 1157 private Integer mSavedOrientation = null; 1158 lockOrientation()1159 private void lockOrientation() { 1160 // when we start recording, lock our orientation 1161 final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); 1162 final WindowManager windowManager = 1163 (WindowManager) a.getSystemService(Context.WINDOW_SERVICE); 1164 final int rotation = windowManager.getDefaultDisplay().getRotation(); 1165 1166 mSavedOrientation = a.getRequestedOrientation(); 1167 switch (rotation) { 1168 case Surface.ROTATION_0: 1169 a.setRequestedOrientation( 1170 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 1171 break; 1172 case Surface.ROTATION_90: 1173 a.setRequestedOrientation( 1174 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 1175 break; 1176 case Surface.ROTATION_180: 1177 a.setRequestedOrientation( 1178 ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); 1179 break; 1180 case Surface.ROTATION_270: 1181 a.setRequestedOrientation( 1182 ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); 1183 break; 1184 } 1185 1186 } 1187 restoreRequestedOrientation()1188 private void restoreRequestedOrientation() { 1189 if (mSavedOrientation != null) { 1190 final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); 1191 if (a != null) { 1192 a.setRequestedOrientation(mSavedOrientation); 1193 } 1194 mSavedOrientation = null; 1195 } 1196 } 1197 hasCameraPermission()1198 static boolean hasCameraPermission() { 1199 return OsUtil.hasPermission(Manifest.permission.CAMERA); 1200 } 1201 } 1202