• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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