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