1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.webauth.authenticator;
6 
7 import android.content.Context;
8 import android.hardware.Camera;
9 import android.util.AttributeSet;
10 import android.view.Display;
11 import android.view.Surface;
12 import android.view.SurfaceHolder;
13 import android.view.SurfaceView;
14 
15 import org.chromium.base.Log;
16 import org.chromium.base.ThreadUtils;
17 import org.chromium.base.task.PostTask;
18 import org.chromium.base.task.TaskTraits;
19 import org.chromium.content_public.browser.UiThreadTaskTraits;
20 
21 import java.io.IOException;
22 
23 /**
24  * Provides a SurfaceView and adapts it for use as a camera preview target
25  * so that the current camera image can be displayed.
26  *
27  * TODO: locking and unlocking the screen seems to stop the camera preview because, on unlock,
28  * multiple of these Views end up getting created and only one wins the race to the camera.
29  */
30 public class CameraView extends SurfaceView implements SurfaceHolder.Callback {
31     private static final String TAG = "CameraView";
32     private Camera.PreviewCallback mCallback;
33     private Display mDisplay;
34 
35     /**
36      * Holds a reference to the camera. Only referenced from the UI thread.
37      */
38     private Camera mCamera;
39     /**
40      * Contains the number of degrees that the image from the selected camera
41      * will be rotated. Only referenced from the UI thread.
42      */
43     private int mCameraRotation;
44     /**
45      * True if a background thread is trying to open the camera. Only referenced from the UI thread.
46      */
47     private boolean mAmOpeningCamera;
48     /**
49       True if this View is currently detached. If this occurs while the camera is being opened
50       then it needs to immediately be closed again. Only referenced from the UI thread.
51     */
52     private boolean mDetached;
53     private SurfaceHolder mHolder;
54 
CameraView(Context context, AttributeSet attributeSet)55     public CameraView(Context context, AttributeSet attributeSet) {
56         super(context, attributeSet);
57     }
58 
setCallback(Camera.PreviewCallback callback)59     public void setCallback(Camera.PreviewCallback callback) {
60         mCallback = callback;
61     }
62 
setDisplay(Display display)63     public void setDisplay(Display display) {
64         mDisplay = display;
65     }
66 
67     /**
68      * Called to indicate that the callback that was passed to the constructor has finished
69      * processing and thus is free to receive another camera frame.
70      */
rearmCallback()71     void rearmCallback() {
72         ThreadUtils.assertOnUiThread();
73 
74         if (mCamera != null) {
75             mCamera.setOneShotPreviewCallback(mCallback);
76         }
77     }
78 
79     @Override
onAttachedToWindow()80     protected void onAttachedToWindow() {
81         ThreadUtils.assertOnUiThread();
82 
83         super.onAttachedToWindow();
84         mDetached = false;
85         getHolder().addCallback(this);
86     }
87 
88     @Override
onDetachedFromWindow()89     protected void onDetachedFromWindow() {
90         ThreadUtils.assertOnUiThread();
91 
92         super.onDetachedFromWindow();
93         mDetached = true;
94         getHolder().removeCallback(this);
95         if (mCamera != null) {
96             mCamera.release();
97             mCamera = null;
98         }
99     }
100 
101     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)102     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
103         // This view is only used in a context where the width is set by the
104         // container.
105         int width = MeasureSpec.getSize(widthMeasureSpec);
106 
107         // The aspect ratio of the camera's preview is only available after
108         // opening it. But that's a slow operation and is performed on a
109         // background thread, while the layout needs to be done immediately.
110         // Therefore assume a 16:9 aspect ratio, which is the most common.
111         // If the ratio turns out to be 4:3, there will be some slight
112         // distortion in the image, but it'll still work.
113         setMeasuredDimension(width, (16 * width) / 9);
114     }
115 
openCamera()116     private void openCamera() {
117         // We want to find the first, rear-facing camera. This is what
118         // Camera.open() gives us, but then we don't get the camera ID and we
119         // need that to get the rotation amount. Thus the need to iterate
120         // over the cameras to find the right one.
121         final int numCameras = Camera.getNumberOfCameras();
122         if (numCameras == 0) {
123             // TODO: indicate in UI when QR scanning fails.
124             return;
125         }
126 
127         Camera.CameraInfo info = new Camera.CameraInfo();
128         boolean found = false;
129         int cameraId;
130         for (cameraId = 0; cameraId < numCameras; cameraId++) {
131             Camera.getCameraInfo(cameraId, info);
132             if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
133                 found = true;
134                 break;
135             }
136         }
137 
138         if (!found) {
139             // No rear facing cameras available. Just use the first camera.
140             cameraId = 0;
141             Camera.getCameraInfo(cameraId, info);
142         }
143 
144         Camera camera;
145         try {
146             camera = Camera.open(cameraId);
147         } catch (RuntimeException e) {
148             Log.w(TAG, "Failed to open camera", e);
149             // TODO: indicate in UI when QR scanning fails.
150             return;
151         }
152 
153         // This logic is based on
154         // https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
155         // But the sample code there appears to be wrong in practice.
156         int rotation;
157         if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
158             rotation = info.orientation;
159         } else {
160             rotation = (360 - info.orientation) % 360;
161         }
162 
163         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> startCamera(camera, rotation));
164     }
165 
startCamera(Camera camera, int cameraRotation)166     private void startCamera(Camera camera, int cameraRotation) {
167         ThreadUtils.assertOnUiThread();
168 
169         mAmOpeningCamera = false;
170 
171         if (mDetached) {
172             // View was detached while the camera was being opened.
173             camera.release();
174             return;
175         }
176 
177         mCamera = camera;
178         mCameraRotation = cameraRotation;
179 
180         if (mHolder == null) {
181             // Surface was lost while the camera was being opened.
182             return;
183         }
184 
185         try {
186             mCamera.setPreviewDisplay(mHolder);
187             // Use a one-shot callback so that callbacks don't happen faster
188             // they're processed.
189             mCamera.setOneShotPreviewCallback(mCallback);
190             Camera.Parameters parameters = mCamera.getParameters();
191             parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
192             mCamera.setParameters(parameters);
193 
194             int displayRotation = 0;
195             // getRotation returns the opposite of the rotation of the physical
196             // display. (I.e. it returns the rotation that needs to be applied
197             // in order to correct for the rotation of the screen.) Thus 90/270
198             // are swapped.
199             switch (mDisplay.getRotation()) {
200                 case Surface.ROTATION_0:
201                     displayRotation = 0;
202                     break;
203                 case Surface.ROTATION_90:
204                     displayRotation = 270;
205                     break;
206                 case Surface.ROTATION_180:
207                     displayRotation = 180;
208                     break;
209                 case Surface.ROTATION_270:
210                     displayRotation = 90;
211                     break;
212             }
213             mCamera.setDisplayOrientation((mCameraRotation + displayRotation) % 360);
214 
215             mCamera.startPreview();
216         } catch (IOException e) {
217             Log.w(TAG, "Exception while starting camera", e);
218         }
219     }
220 
221     /** SurfaceHolder.Callback implementation. */
222     @Override
surfaceCreated(SurfaceHolder holder)223     public void surfaceCreated(SurfaceHolder holder) {
224         ThreadUtils.assertOnUiThread();
225 
226         mHolder = holder;
227         if (mAmOpeningCamera) {
228             return;
229         }
230 
231         if (mCamera == null) {
232             mAmOpeningCamera = true;
233             PostTask.postTask(TaskTraits.USER_VISIBLE_MAY_BLOCK, this::openCamera);
234         } else {
235             startCamera(mCamera, mCameraRotation);
236         }
237     }
238 
239     @Override
surfaceDestroyed(SurfaceHolder holder)240     public void surfaceDestroyed(SurfaceHolder holder) {
241         ThreadUtils.assertOnUiThread();
242 
243         mHolder = null;
244         if (mCamera != null) {
245             mCamera.setOneShotPreviewCallback(null);
246             mCamera.stopPreview();
247         }
248     }
249 
250     @Override
surfaceChanged(SurfaceHolder holder, int format, int w, int h)251     public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
252         surfaceDestroyed(holder);
253         surfaceCreated(holder);
254     }
255 }
256