1 /*
2  * Copyright (C) 2021 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.systemui.biometrics;
18 
19 import android.annotation.IdRes;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.graphics.Insets;
23 import android.graphics.Rect;
24 import android.hardware.biometrics.SensorLocationInternal;
25 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
26 import android.os.Build;
27 import android.util.Log;
28 import android.view.Surface;
29 import android.view.View;
30 import android.view.View.MeasureSpec;
31 import android.view.ViewGroup;
32 import android.view.WindowInsets;
33 import android.view.WindowManager;
34 import android.view.WindowMetrics;
35 import android.widget.FrameLayout;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.systemui.R;
39 
40 /**
41  * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical
42  * under-display fingerprint sensor (UDFPS).
43  */
44 public class UdfpsDialogMeasureAdapter {
45     private static final String TAG = "UdfpsDialogMeasurementAdapter";
46     private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;
47 
48     @NonNull private final ViewGroup mView;
49     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
50     @Nullable private WindowManager mWindowManager;
51     private int mBottomSpacerHeight;
52 
UdfpsDialogMeasureAdapter( @onNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps)53     public UdfpsDialogMeasureAdapter(
54             @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) {
55         mView = view;
56         mSensorProps = sensorProps;
57         mWindowManager = mView.getContext().getSystemService(WindowManager.class);
58     }
59 
60     @NonNull
getSensorProps()61     FingerprintSensorPropertiesInternal getSensorProps() {
62         return mSensorProps;
63     }
64 
65     @NonNull
onMeasureInternal( int width, int height, @NonNull AuthDialog.LayoutParams layoutParams, float scaleFactor)66     public AuthDialog.LayoutParams onMeasureInternal(
67             int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
68             float scaleFactor) {
69 
70         final int displayRotation = mView.getDisplay().getRotation();
71         switch (displayRotation) {
72             case Surface.ROTATION_0:
73                 return onMeasureInternalPortrait(width, height, scaleFactor);
74             case Surface.ROTATION_90:
75             case Surface.ROTATION_270:
76                 return onMeasureInternalLandscape(width, height, scaleFactor);
77             default:
78                 Log.e(TAG, "Unsupported display rotation: " + displayRotation);
79                 return layoutParams;
80         }
81     }
82 
83     /**
84      * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates
85      * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard
86      * too cleanly support this case. So, let's have the onLayout code translate the sensor location
87      * instead.
88      */
getBottomSpacerHeight()89     public int getBottomSpacerHeight() {
90         return mBottomSpacerHeight;
91     }
92 
93     /**
94      * @return sensor diameter size as scaleFactor
95      */
getSensorDiameter(float scaleFactor)96     public int getSensorDiameter(float scaleFactor) {
97         return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2);
98     }
99 
100     @NonNull
onMeasureInternalPortrait(int width, int height, float scaleFactor)101     private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height,
102             float scaleFactor) {
103         final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();
104 
105         // Figure out where the bottom of the sensor anim should be.
106         final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
107         final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
108         final int dialogMargin = getDialogMarginPx();
109         final int displayHeight = getMaximumWindowBounds(windowMetrics).height();
110         final Insets navbarInsets = getNavbarInsets(windowMetrics);
111         mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait(
112                 mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight,
113                 dialogMargin, navbarInsets.bottom, scaleFactor);
114 
115         // Go through each of the children and do the custom measurement.
116         int totalHeight = 0;
117         final int numChildren = mView.getChildCount();
118         final int sensorDiameter = getSensorDiameter(scaleFactor);
119         for (int i = 0; i < numChildren; i++) {
120             final View child = mView.getChildAt(i);
121             if (child.getId() == R.id.biometric_icon_frame) {
122                 final FrameLayout iconFrame = (FrameLayout) child;
123                 final View icon = iconFrame.getChildAt(0);
124                 // Create a frame that's exactly the height of the sensor circle.
125                 iconFrame.measure(
126                         MeasureSpec.makeMeasureSpec(
127                                 child.getLayoutParams().width, MeasureSpec.EXACTLY),
128                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));
129 
130                 // Ensure that the icon is never larger than the sensor.
131                 icon.measure(
132                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
133                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
134             } else if (child.getId() == R.id.space_above_icon) {
135                 child.measure(
136                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
137                         MeasureSpec.makeMeasureSpec(
138                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
139             } else if (child.getId() == R.id.button_bar) {
140                 child.measure(
141                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
142                         MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
143                                 MeasureSpec.EXACTLY));
144             } else if (child.getId() == R.id.space_below_icon) {
145                 // Set the spacer height so the fingerprint icon is on the physical sensor area
146                 final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
147                 child.measure(
148                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
149                         MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
150             } else if (child.getId() == R.id.description) {
151                 //skip description view and compute later
152                 continue;
153             } else {
154                 child.measure(
155                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
156                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
157             }
158 
159             if (child.getVisibility() != View.GONE) {
160                 totalHeight += child.getMeasuredHeight();
161             }
162         }
163 
164         //re-calculate the height of description
165         View description = mView.findViewById(R.id.description);
166         if (description != null && description.getVisibility() != View.GONE) {
167             totalHeight += measureDescription(description, displayHeight, width, totalHeight);
168         }
169 
170         return new AuthDialog.LayoutParams(width, totalHeight);
171     }
172 
measureDescription(View description, int displayHeight, int currWidth, int currHeight)173     private int measureDescription(View description, int displayHeight, int currWidth,
174                                    int currHeight) {
175         //description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
176         //so we could getMeasuredHeight in onMeasureInternalPortrait directly.
177         int newHeight = description.getMeasuredHeight() + currHeight;
178         int limit = (int) (displayHeight * 0.75);
179         if (newHeight > limit) {
180             description.measure(
181                     MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
182                     MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
183         }
184         return description.getMeasuredHeight();
185     }
186 
187     @NonNull
onMeasureInternalLandscape(int width, int height, float scaleFactor)188     private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height,
189             float scaleFactor) {
190         final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();
191 
192         // Find the spacer height needed to vertically align the icon with the sensor.
193         final int titleHeight = getViewHeightPx(R.id.title);
194         final int subtitleHeight = getViewHeightPx(R.id.subtitle);
195         final int descriptionHeight = getViewHeightPx(R.id.description);
196         final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon);
197         final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
198         final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
199 
200         final Insets navbarInsets = getNavbarInsets(windowMetrics);
201         final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight,
202                 subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight,
203                 buttonBarHeight, navbarInsets.bottom);
204 
205         // Find the spacer width needed to horizontally align the icon with the sensor.
206         final int displayWidth = getMaximumWindowBounds(windowMetrics).width();
207         final int dialogMargin = getDialogMarginPx();
208         final int horizontalInset = navbarInsets.left + navbarInsets.right;
209         final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape(
210                 mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor);
211 
212         final int sensorDiameter = getSensorDiameter(scaleFactor);
213         final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth;
214 
215         int remeasuredHeight = 0;
216         final int numChildren = mView.getChildCount();
217         for (int i = 0; i < numChildren; i++) {
218             final View child = mView.getChildAt(i);
219             if (child.getId() == R.id.biometric_icon_frame) {
220                 final FrameLayout iconFrame = (FrameLayout) child;
221                 final View icon = iconFrame.getChildAt(0);
222                 // Create a frame that's exactly the height of the sensor circle.
223                 iconFrame.measure(
224                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
225                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));
226 
227                 // Ensure that the icon is never larger than the sensor.
228                 icon.measure(
229                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
230                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
231             } else if (child.getId() == R.id.space_above_icon) {
232                 // Adjust the width and height of the top spacer if necessary.
233                 final int newTopSpacerHeight = child.getLayoutParams().height
234                         - Math.min(bottomSpacerHeight, 0);
235                 child.measure(
236                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
237                         MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY));
238             } else if (child.getId() == R.id.button_bar) {
239                 // Adjust the width of the button bar while preserving its height.
240                 child.measure(
241                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
242                         MeasureSpec.makeMeasureSpec(
243                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
244             } else if (child.getId() == R.id.space_below_icon) {
245                 // Adjust the bottom spacer height to align the fingerprint icon with the sensor.
246                 final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0);
247                 child.measure(
248                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
249                         MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY));
250             } else {
251                 // Use the remeasured width for all other child views.
252                 child.measure(
253                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
254                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
255             }
256 
257             if (child.getVisibility() != View.GONE) {
258                 remeasuredHeight += child.getMeasuredHeight();
259             }
260         }
261 
262         return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight);
263     }
264 
getViewHeightPx(@dRes int viewId)265     private int getViewHeightPx(@IdRes int viewId) {
266         final View view = mView.findViewById(viewId);
267         return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0;
268     }
269 
getDialogMarginPx()270     private int getDialogMarginPx() {
271         return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding);
272     }
273 
274     @NonNull
getNavbarInsets(@ullable WindowMetrics windowMetrics)275     private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) {
276         return windowMetrics != null
277                 ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars())
278                 : Insets.NONE;
279     }
280 
281     @NonNull
getMaximumWindowBounds(@ullable WindowMetrics windowMetrics)282     private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) {
283         return windowMetrics != null ? windowMetrics.getBounds() : new Rect();
284     }
285 
286     /**
287      * For devices in portrait orientation where the sensor is too high up, calculates the amount of
288      * padding necessary to center the biometric icon within the sensor's physical location.
289      */
290     @VisibleForTesting
calculateBottomSpacerHeightForPortrait( @onNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, int navbarBottomInsetPx, float scaleFactor)291     static int calculateBottomSpacerHeightForPortrait(
292             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx,
293             int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx,
294             int navbarBottomInsetPx, float scaleFactor) {
295         final SensorLocationInternal location = sensorProperties.getLocation();
296         final int sensorDistanceFromBottom = displayHeightPx
297                 - (int) (scaleFactor * location.sensorLocationY)
298                 - (int) (scaleFactor * location.sensorRadius);
299 
300         final int spacerHeight = sensorDistanceFromBottom
301                 - textIndicatorHeightPx
302                 - buttonBarHeightPx
303                 - dialogMarginPx
304                 - navbarBottomInsetPx;
305 
306         if (DEBUG) {
307             Log.d(TAG, "Display height: " + displayHeightPx
308                     + ", Distance from bottom: " + sensorDistanceFromBottom
309                     + ", Bottom margin: " + dialogMarginPx
310                     + ", Navbar bottom inset: " + navbarBottomInsetPx
311                     + ", Bottom spacer height (portrait): " + spacerHeight
312                     + ", Scale Factor: " + scaleFactor);
313         }
314 
315         return spacerHeight;
316     }
317 
318     /**
319      * For devices in landscape orientation where the sensor is too high up, calculates the amount
320      * of padding necessary to center the biometric icon within the sensor's physical location.
321      */
322     @VisibleForTesting
calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int navbarBottomInsetPx)323     static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx,
324             int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx,
325             int buttonBarHeightPx, int navbarBottomInsetPx) {
326 
327         final int dialogHeightAboveIcon = titleHeightPx
328                 + subtitleHeightPx
329                 + descriptionHeightPx
330                 + topSpacerHeightPx;
331 
332         final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx;
333 
334         final int bottomSpacerHeight = dialogHeightAboveIcon
335                 - dialogHeightBelowIcon
336                 - navbarBottomInsetPx;
337 
338         if (DEBUG) {
339             Log.d(TAG, "Title height: " + titleHeightPx
340                     + ", Subtitle height: " + subtitleHeightPx
341                     + ", Description height: " + descriptionHeightPx
342                     + ", Top spacer height: " + topSpacerHeightPx
343                     + ", Text indicator height: " + textIndicatorHeightPx
344                     + ", Button bar height: " + buttonBarHeightPx
345                     + ", Navbar bottom inset: " + navbarBottomInsetPx
346                     + ", Bottom spacer height (landscape): " + bottomSpacerHeight);
347         }
348 
349         return bottomSpacerHeight;
350     }
351 
352     /**
353      * For devices in landscape orientation where the sensor is too left/right, calculates the
354      * amount of padding necessary to center the biometric icon within the sensor's physical
355      * location.
356      */
357     @VisibleForTesting
calculateHorizontalSpacerWidthForLandscape( @onNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor)358     static int calculateHorizontalSpacerWidthForLandscape(
359             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx,
360             int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) {
361         final SensorLocationInternal location = sensorProperties.getLocation();
362         final int sensorDistanceFromEdge = displayWidthPx
363                 - (int) (scaleFactor * location.sensorLocationY)
364                 - (int) (scaleFactor * location.sensorRadius);
365 
366         final int horizontalPadding = sensorDistanceFromEdge
367                 - dialogMarginPx
368                 - navbarHorizontalInsetPx;
369 
370         if (DEBUG) {
371             Log.d(TAG, "Display width: " + displayWidthPx
372                     + ", Distance from edge: " + sensorDistanceFromEdge
373                     + ", Dialog margin: " + dialogMarginPx
374                     + ", Navbar horizontal inset: " + navbarHorizontalInsetPx
375                     + ", Horizontal spacer width (landscape): " + horizontalPadding
376                     + ", Scale Factor: " + scaleFactor);
377         }
378 
379         return horizontalPadding;
380     }
381 }
382