1 /* 2 * Copyright (C) 2018 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.settings.biometrics.face; 18 19 import android.app.settings.SettingsEnums; 20 import android.content.Context; 21 import android.graphics.Matrix; 22 import android.graphics.SurfaceTexture; 23 import android.hardware.camera2.CameraAccessException; 24 import android.hardware.camera2.CameraCaptureSession; 25 import android.hardware.camera2.CameraCharacteristics; 26 import android.hardware.camera2.CameraDevice; 27 import android.hardware.camera2.CameraManager; 28 import android.hardware.camera2.CaptureRequest; 29 import android.hardware.camera2.params.StreamConfigurationMap; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.util.Log; 34 import android.util.Size; 35 import android.util.TypedValue; 36 import android.view.Surface; 37 import android.view.TextureView; 38 import android.view.View; 39 import android.widget.ImageView; 40 41 import com.android.settings.R; 42 import com.android.settings.biometrics.BiometricEnrollSidecar; 43 import com.android.settings.core.InstrumentedPreferenceFragment; 44 45 import java.util.Arrays; 46 47 /** 48 * Fragment that contains the logic for showing and controlling the camera preview, circular 49 * overlay, as well as the enrollment animations. 50 */ 51 public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment 52 implements BiometricEnrollSidecar.Listener { 53 54 private static final String TAG = "FaceEnrollPreviewFragment"; 55 56 private static final int MAX_PREVIEW_WIDTH = 1920; 57 private static final int MAX_PREVIEW_HEIGHT = 1080; 58 59 private Handler mHandler = new Handler(Looper.getMainLooper()); 60 private CameraManager mCameraManager; 61 private String mCameraId; 62 private CameraDevice mCameraDevice; 63 private CaptureRequest.Builder mPreviewRequestBuilder; 64 private CameraCaptureSession mCaptureSession; 65 private CaptureRequest mPreviewRequest; 66 private Size mPreviewSize; 67 private ParticleCollection.Listener mListener; 68 69 // View used to contain the circular cutout and enrollment animation drawable 70 private ImageView mCircleView; 71 72 // Drawable containing the circular cutout and enrollment animations 73 private FaceEnrollAnimationDrawable mAnimationDrawable; 74 75 // Texture used for showing the camera preview 76 private FaceSquareTextureView mTextureView; 77 78 // Listener sent to the animation drawable 79 private final ParticleCollection.Listener mAnimationListener 80 = new ParticleCollection.Listener() { 81 @Override 82 public void onEnrolled() { 83 mListener.onEnrolled(); 84 } 85 }; 86 87 private final TextureView.SurfaceTextureListener mSurfaceTextureListener = 88 new TextureView.SurfaceTextureListener() { 89 90 @Override 91 public void onSurfaceTextureAvailable( 92 SurfaceTexture surfaceTexture, int width, int height) { 93 openCamera(width, height); 94 } 95 96 @Override 97 public void onSurfaceTextureSizeChanged( 98 SurfaceTexture surfaceTexture, int width, int height) { 99 // Shouldn't be called, but do this for completeness. 100 configureTransform(width, height); 101 } 102 103 @Override 104 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 105 return true; 106 } 107 108 @Override 109 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 110 111 } 112 }; 113 114 private final CameraDevice.StateCallback mCameraStateCallback = 115 new CameraDevice.StateCallback() { 116 117 @Override 118 public void onOpened(CameraDevice cameraDevice) { 119 mCameraDevice = cameraDevice; 120 try { 121 // Configure the size of default buffer 122 SurfaceTexture texture = mTextureView.getSurfaceTexture(); 123 texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); 124 125 // This is the output Surface we need to start preview 126 Surface surface = new Surface(texture); 127 128 // Set up a CaptureRequest.Builder with the output Surface 129 mPreviewRequestBuilder = 130 mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); 131 mPreviewRequestBuilder.addTarget(surface); 132 133 // Create a CameraCaptureSession for camera preview 134 mCameraDevice.createCaptureSession(Arrays.asList(surface), 135 new CameraCaptureSession.StateCallback() { 136 137 @Override 138 public void onConfigured(CameraCaptureSession cameraCaptureSession) { 139 // The camera is already closed 140 if (null == mCameraDevice) { 141 return; 142 } 143 // When the session is ready, we start displaying the preview. 144 mCaptureSession = cameraCaptureSession; 145 try { 146 // Auto focus should be continuous for camera preview. 147 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, 148 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); 149 150 // Finally, we start displaying the camera preview. 151 mPreviewRequest = mPreviewRequestBuilder.build(); 152 mCaptureSession.setRepeatingRequest(mPreviewRequest, 153 null /* listener */, mHandler); 154 } catch (CameraAccessException e) { 155 Log.e(TAG, "Unable to access camera", e); 156 } 157 } 158 159 @Override 160 public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { 161 Log.e(TAG, "Unable to configure camera"); 162 } 163 }, null /* handler */); 164 } catch (CameraAccessException e) { 165 e.printStackTrace(); 166 } 167 } 168 169 @Override 170 public void onDisconnected(CameraDevice cameraDevice) { 171 cameraDevice.close(); 172 mCameraDevice = null; 173 } 174 175 @Override 176 public void onError(CameraDevice cameraDevice, int error) { 177 cameraDevice.close(); 178 mCameraDevice = null; 179 } 180 }; 181 182 @Override getMetricsCategory()183 public int getMetricsCategory() { 184 return SettingsEnums.FACE_ENROLL_PREVIEW; 185 } 186 187 @Override onCreate(Bundle savedInstanceState)188 public void onCreate(Bundle savedInstanceState) { 189 super.onCreate(savedInstanceState); 190 mTextureView = getActivity().findViewById(R.id.texture_view); 191 mCircleView = getActivity().findViewById(R.id.circle_view); 192 193 // Must disable hardware acceleration for this view, otherwise transparency breaks 194 mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 195 196 mAnimationDrawable = new FaceEnrollAnimationDrawable(getContext(), mAnimationListener); 197 mCircleView.setImageDrawable(mAnimationDrawable); 198 199 mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); 200 } 201 202 @Override onResume()203 public void onResume() { 204 super.onResume(); 205 206 // When the screen is turned off and turned back on, the SurfaceTexture is already 207 // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open 208 // a camera and start preview from here (otherwise, we wait until the surface is ready in 209 // the SurfaceTextureListener). 210 if (mTextureView.isAvailable()) { 211 openCamera(mTextureView.getWidth(), mTextureView.getHeight()); 212 } else { 213 mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); 214 } 215 } 216 217 @Override onPause()218 public void onPause() { 219 super.onPause(); 220 closeCamera(); 221 } 222 223 @Override onEnrollmentError(int errMsgId, CharSequence errString)224 public void onEnrollmentError(int errMsgId, CharSequence errString) { 225 mAnimationDrawable.onEnrollmentError(errMsgId, errString); 226 } 227 228 @Override onEnrollmentHelp(int helpMsgId, CharSequence helpString)229 public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { 230 mAnimationDrawable.onEnrollmentHelp(helpMsgId, helpString); 231 } 232 233 @Override onEnrollmentProgressChange(int steps, int remaining)234 public void onEnrollmentProgressChange(int steps, int remaining) { 235 mAnimationDrawable.onEnrollmentProgressChange(steps, remaining); 236 } 237 setListener(ParticleCollection.Listener listener)238 public void setListener(ParticleCollection.Listener listener) { 239 mListener = listener; 240 } 241 242 /** 243 * Sets up member variables related to camera. 244 */ setUpCameraOutputs()245 private void setUpCameraOutputs() { 246 try { 247 for (String cameraId : mCameraManager.getCameraIdList()) { 248 CameraCharacteristics characteristics = 249 mCameraManager.getCameraCharacteristics(cameraId); 250 251 // Find front facing camera 252 Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); 253 if (facing == null || facing != CameraCharacteristics.LENS_FACING_FRONT) { 254 continue; 255 } 256 mCameraId = cameraId; 257 258 // Get the stream configurations 259 StreamConfigurationMap map = characteristics.get( 260 CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); 261 mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class)); 262 break; 263 } 264 } catch (CameraAccessException e) { 265 Log.e(TAG, "Unable to access camera", e); 266 } 267 } 268 269 /** 270 * Opens the camera specified by mCameraId. 271 * @param width The width of the texture view 272 * @param height The height of the texture view 273 */ openCamera(int width, int height)274 private void openCamera(int width, int height) { 275 try { 276 setUpCameraOutputs(); 277 mCameraManager.openCamera(mCameraId, mCameraStateCallback, mHandler); 278 configureTransform(width, height); 279 } catch (CameraAccessException e) { 280 Log.e(TAG, "Unable to open camera", e); 281 } 282 } 283 284 /** 285 * Chooses the optimal resolution for the camera to open. 286 */ chooseOptimalSize(Size[] choices)287 private Size chooseOptimalSize(Size[] choices) { 288 for (int i = 0; i < choices.length; i++) { 289 if (choices[i].getHeight() == MAX_PREVIEW_HEIGHT 290 && choices[i].getWidth() == MAX_PREVIEW_WIDTH) { 291 return choices[i]; 292 } 293 } 294 Log.w(TAG, "Unable to find a good resolution"); 295 return choices[0]; 296 } 297 298 /** 299 * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`. 300 * This method should be called after the camera preview size is determined in 301 * setUpCameraOutputs and also the size of `mTextureView` is fixed. 302 * 303 * @param viewWidth The width of `mTextureView` 304 * @param viewHeight The height of `mTextureView` 305 */ configureTransform(int viewWidth, int viewHeight)306 private void configureTransform(int viewWidth, int viewHeight) { 307 if (mTextureView == null) { 308 return; 309 } 310 311 // Fix the aspect ratio 312 float scaleX = (float) viewWidth / mPreviewSize.getWidth(); 313 float scaleY = (float) viewHeight / mPreviewSize.getHeight(); 314 315 // Now divide by smaller one so it fills up the original space. 316 float smaller = Math.min(scaleX, scaleY); 317 scaleX = scaleX / smaller; 318 scaleY = scaleY / smaller; 319 320 final TypedValue tx = new TypedValue(); 321 final TypedValue ty = new TypedValue(); 322 final TypedValue scale = new TypedValue(); 323 getResources().getValue(R.dimen.face_preview_translate_x, tx, true /* resolveRefs */); 324 getResources().getValue(R.dimen.face_preview_translate_y, ty, true /* resolveRefs */); 325 getResources().getValue(R.dimen.face_preview_scale, scale, true /* resolveRefs */); 326 327 // Apply the transformation/scale 328 final Matrix transform = new Matrix(); 329 mTextureView.getTransform(transform); 330 transform.setScale(scaleX * scale.getFloat(), scaleY * scale.getFloat()); 331 transform.postTranslate(tx.getFloat(), ty.getFloat()); 332 mTextureView.setTransform(transform); 333 } 334 closeCamera()335 private void closeCamera() { 336 if (mCaptureSession != null) { 337 mCaptureSession.close(); 338 mCaptureSession = null; 339 } 340 if (mCameraDevice != null) { 341 mCameraDevice.close(); 342 mCameraDevice = null; 343 } 344 } 345 } 346