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