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