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