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