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