• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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 */
updateCameraOrientation()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 */
setCamera(final Camera 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 */
tryShowPreview()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 
tryInitOrCleanupVideoMode()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 
tryStartVideoCapture()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 
stopVideo()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 
isCameraAvailable()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      */
reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported)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 */
getScaleFactorForMaxAllowedSize(final int width, final int height, final int maxWidth, final int maxHeight)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 
getMmsConfig()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      */
chooseBestPictureSize()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      */
chooseBestPreviewSize(final Camera.Size pictureSize)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 {
OrientationHandler(final Context context)1025         OrientationHandler(final Context context) {
1026             super(context);
1027         }
1028 
1029         @Override
onOrientationChanged(final int orientation)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 
SizeComparator(final int maxWidth, final int maxHeight, final float targetAspectRatio, final int targetPixels)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
compare(final Camera.Size left, final Camera.Size right)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
autoFocus()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
cancelAutoFocus()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
capture()1123     public boolean capture() {
1124         return false;
1125     }
1126 
1127     @Override // From FocusOverlayManager.Listener
setFocusParameters()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 
logCameraSize(final String prefix, final Camera.Size size)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 
lockOrientation()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 
restoreRequestedOrientation()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 
hasCameraPermission()1197     static boolean hasCameraPermission() {
1198         return OsUtil.hasPermission(Manifest.permission.CAMERA);
1199     }
1200 }
1201