• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.res.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.getId() == R.id.space_above_content
136                     || child.getId() == R.id.button_bar) {
137                 child.measure(
138                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
139                         MeasureSpec.makeMeasureSpec(
140                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
141             } else if (child.getId() == R.id.space_below_icon) {
142                 // Set the spacer height so the fingerprint icon is on the physical sensor area
143                 final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
144                 child.measure(
145                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
146                         MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
147             } else if (child.getId() == R.id.description
148                     || child.getId() == R.id.customized_view_container) {
149                 //skip description view and compute later
150                 continue;
151             } else if (child.getId() == R.id.logo) {
152                 child.measure(
153                         MeasureSpec.makeMeasureSpec(child.getLayoutParams().width,
154                                 MeasureSpec.EXACTLY),
155                         MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
156                                 MeasureSpec.EXACTLY));
157             } else {
158                 child.measure(
159                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
160                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
161             }
162 
163             if (child.getVisibility() != View.GONE) {
164                 totalHeight += child.getMeasuredHeight();
165             }
166         }
167 
168         //re-calculate the height of body content
169         View description = mView.findViewById(R.id.description);
170         View contentView = mView.findViewById(R.id.customized_view_container);
171         if (description != null && description.getVisibility() != View.GONE) {
172             totalHeight += measureDescription(description, displayHeight, width, totalHeight);
173         } else if (contentView != null && contentView.getVisibility() != View.GONE) {
174             totalHeight += measureDescription(contentView, displayHeight, width, totalHeight);
175         }
176 
177         return new AuthDialog.LayoutParams(width, totalHeight);
178     }
179 
measureDescription(View bodyContent, int displayHeight, int currWidth, int currHeight)180     private int measureDescription(View bodyContent, int displayHeight, int currWidth,
181                                    int currHeight) {
182         int newHeight = bodyContent.getMeasuredHeight() + currHeight;
183         int limit = (int) (displayHeight * 0.75);
184         if (newHeight > limit) {
185             bodyContent.measure(
186                     MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
187                     MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
188         }
189         return bodyContent.getMeasuredHeight();
190     }
191 
192     @NonNull
onMeasureInternalLandscape(int width, int height, float scaleFactor)193     private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height,
194             float scaleFactor) {
195         final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();
196 
197         // Find the spacer height needed to vertically align the icon with the sensor.
198         final int titleHeight = getViewHeightPx(R.id.title);
199         final int subtitleHeight = getViewHeightPx(R.id.subtitle);
200         final int descriptionHeight = getViewHeightPx(R.id.description);
201         final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon);
202         final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
203         final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
204 
205         final Insets navbarInsets = getNavbarInsets(windowMetrics);
206         final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight,
207                 subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight,
208                 buttonBarHeight, navbarInsets.bottom);
209 
210         // Find the spacer width needed to horizontally align the icon with the sensor.
211         final int displayWidth = getMaximumWindowBounds(windowMetrics).width();
212         final int dialogMargin = getDialogMarginPx();
213         final int horizontalInset = navbarInsets.left + navbarInsets.right;
214         final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape(
215                 mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor);
216 
217         final int sensorDiameter = getSensorDiameter(scaleFactor);
218         final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth;
219 
220         int remeasuredHeight = 0;
221         final int numChildren = mView.getChildCount();
222         for (int i = 0; i < numChildren; i++) {
223             final View child = mView.getChildAt(i);
224             if (child.getId() == R.id.biometric_icon_frame) {
225                 final FrameLayout iconFrame = (FrameLayout) child;
226                 final View icon = iconFrame.getChildAt(0);
227                 // Create a frame that's exactly the height of the sensor circle.
228                 iconFrame.measure(
229                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
230                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));
231 
232                 // Ensure that the icon is never larger than the sensor.
233                 icon.measure(
234                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
235                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
236             } else if (child.getId() == R.id.space_above_icon) {
237                 // Adjust the width and height of the top spacer if necessary.
238                 final int newTopSpacerHeight = child.getLayoutParams().height
239                         - Math.min(bottomSpacerHeight, 0);
240                 child.measure(
241                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
242                         MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY));
243             } else if (child.getId() == R.id.button_bar) {
244                 // Adjust the width of the button bar while preserving its height.
245                 child.measure(
246                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
247                         MeasureSpec.makeMeasureSpec(
248                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
249             } else if (child.getId() == R.id.space_below_icon) {
250                 // Adjust the bottom spacer height to align the fingerprint icon with the sensor.
251                 final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0);
252                 child.measure(
253                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
254                         MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY));
255             } else {
256                 // Use the remeasured width for all other child views.
257                 child.measure(
258                         MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
259                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
260             }
261 
262             if (child.getVisibility() != View.GONE) {
263                 remeasuredHeight += child.getMeasuredHeight();
264             }
265         }
266 
267         return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight);
268     }
269 
getViewHeightPx(@dRes int viewId)270     private int getViewHeightPx(@IdRes int viewId) {
271         final View view = mView.findViewById(viewId);
272         return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0;
273     }
274 
getDialogMarginPx()275     private int getDialogMarginPx() {
276         return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding);
277     }
278 
279     @NonNull
getNavbarInsets(@ullable WindowMetrics windowMetrics)280     private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) {
281         return windowMetrics != null
282                 ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars())
283                 : Insets.NONE;
284     }
285 
286     @NonNull
getMaximumWindowBounds(@ullable WindowMetrics windowMetrics)287     private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) {
288         return windowMetrics != null ? windowMetrics.getBounds() : new Rect();
289     }
290 
291     /**
292      * For devices in portrait orientation where the sensor is too high up, calculates the amount of
293      * padding necessary to center the biometric icon within the sensor's physical location.
294      */
295     @VisibleForTesting
calculateBottomSpacerHeightForPortrait( @onNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, int navbarBottomInsetPx, float scaleFactor)296     static int calculateBottomSpacerHeightForPortrait(
297             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx,
298             int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx,
299             int navbarBottomInsetPx, float scaleFactor) {
300         final SensorLocationInternal location = sensorProperties.getLocation();
301         final int sensorDistanceFromBottom = displayHeightPx
302                 - (int) (scaleFactor * location.sensorLocationY)
303                 - (int) (scaleFactor * location.sensorRadius);
304 
305         final int spacerHeight = sensorDistanceFromBottom
306                 - textIndicatorHeightPx
307                 - buttonBarHeightPx
308                 - dialogMarginPx
309                 - navbarBottomInsetPx;
310 
311         if (DEBUG) {
312             Log.d(TAG, "Display height: " + displayHeightPx
313                     + ", Distance from bottom: " + sensorDistanceFromBottom
314                     + ", Bottom margin: " + dialogMarginPx
315                     + ", Navbar bottom inset: " + navbarBottomInsetPx
316                     + ", Bottom spacer height (portrait): " + spacerHeight
317                     + ", Scale Factor: " + scaleFactor);
318         }
319 
320         return spacerHeight;
321     }
322 
323     /**
324      * For devices in landscape orientation where the sensor is too high up, calculates the amount
325      * of padding necessary to center the biometric icon within the sensor's physical location.
326      */
327     @VisibleForTesting
calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int navbarBottomInsetPx)328     static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx,
329             int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx,
330             int buttonBarHeightPx, int navbarBottomInsetPx) {
331 
332         final int dialogHeightAboveIcon = titleHeightPx
333                 + subtitleHeightPx
334                 + descriptionHeightPx
335                 + topSpacerHeightPx;
336 
337         final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx;
338 
339         final int bottomSpacerHeight = dialogHeightAboveIcon
340                 - dialogHeightBelowIcon
341                 - navbarBottomInsetPx;
342 
343         if (DEBUG) {
344             Log.d(TAG, "Title height: " + titleHeightPx
345                     + ", Subtitle height: " + subtitleHeightPx
346                     + ", Description height: " + descriptionHeightPx
347                     + ", Top spacer height: " + topSpacerHeightPx
348                     + ", Text indicator height: " + textIndicatorHeightPx
349                     + ", Button bar height: " + buttonBarHeightPx
350                     + ", Navbar bottom inset: " + navbarBottomInsetPx
351                     + ", Bottom spacer height (landscape): " + bottomSpacerHeight);
352         }
353 
354         return bottomSpacerHeight;
355     }
356 
357     /**
358      * For devices in landscape orientation where the sensor is too left/right, calculates the
359      * amount of padding necessary to center the biometric icon within the sensor's physical
360      * location.
361      */
362     @VisibleForTesting
calculateHorizontalSpacerWidthForLandscape( @onNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor)363     static int calculateHorizontalSpacerWidthForLandscape(
364             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx,
365             int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) {
366         final SensorLocationInternal location = sensorProperties.getLocation();
367         final int sensorDistanceFromEdge = displayWidthPx
368                 - (int) (scaleFactor * location.sensorLocationY)
369                 - (int) (scaleFactor * location.sensorRadius);
370 
371         final int horizontalPadding = sensorDistanceFromEdge
372                 - dialogMarginPx
373                 - navbarHorizontalInsetPx;
374 
375         if (DEBUG) {
376             Log.d(TAG, "Display width: " + displayWidthPx
377                     + ", Distance from edge: " + sensorDistanceFromEdge
378                     + ", Dialog margin: " + dialogMarginPx
379                     + ", Navbar horizontal inset: " + navbarHorizontalInsetPx
380                     + ", Horizontal spacer width (landscape): " + horizontalPadding
381                     + ", Scale Factor: " + scaleFactor);
382         }
383 
384         return horizontalPadding;
385     }
386 }
387