/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.biometrics; import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.biometrics.SensorLocationInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Build; import android.util.Log; import android.view.Surface; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.widget.FrameLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.res.R; /** * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical * under-display fingerprint sensor (UDFPS). */ public class UdfpsDialogMeasureAdapter { private static final String TAG = "UdfpsDialogMeasurementAdapter"; private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG; @NonNull private final ViewGroup mView; @NonNull private final FingerprintSensorPropertiesInternal mSensorProps; @Nullable private WindowManager mWindowManager; private int mBottomSpacerHeight; public UdfpsDialogMeasureAdapter( @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) { mView = view; mSensorProps = sensorProps; mWindowManager = mView.getContext().getSystemService(WindowManager.class); } @NonNull FingerprintSensorPropertiesInternal getSensorProps() { return mSensorProps; } @NonNull public AuthDialog.LayoutParams onMeasureInternal( int width, int height, @NonNull AuthDialog.LayoutParams layoutParams, float scaleFactor) { final int displayRotation = mView.getDisplay().getRotation(); switch (displayRotation) { case Surface.ROTATION_0: return onMeasureInternalPortrait(width, height, scaleFactor); case Surface.ROTATION_90: case Surface.ROTATION_270: return onMeasureInternalLandscape(width, height, scaleFactor); default: Log.e(TAG, "Unsupported display rotation: " + displayRotation); return layoutParams; } } /** * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard * too cleanly support this case. So, let's have the onLayout code translate the sensor location * instead. */ public int getBottomSpacerHeight() { return mBottomSpacerHeight; } /** * @return sensor diameter size as scaleFactor */ public int getSensorDiameter(float scaleFactor) { return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2); } @NonNull private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height, float scaleFactor) { final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); // Figure out where the bottom of the sensor anim should be. final int textIndicatorHeight = getViewHeightPx(R.id.indicator); final int buttonBarHeight = getViewHeightPx(R.id.button_bar); final int dialogMargin = getDialogMarginPx(); final int displayHeight = getMaximumWindowBounds(windowMetrics).height(); final Insets navbarInsets = getNavbarInsets(windowMetrics); mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait( mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, dialogMargin, navbarInsets.bottom, scaleFactor); // Go through each of the children and do the custom measurement. int totalHeight = 0; final int numChildren = mView.getChildCount(); final int sensorDiameter = getSensorDiameter(scaleFactor); for (int i = 0; i < numChildren; i++) { final View child = mView.getChildAt(i); if (child.getId() == R.id.biometric_icon_frame) { final FrameLayout iconFrame = (FrameLayout) child; final View icon = iconFrame.getChildAt(0); // Create a frame that's exactly the height of the sensor circle. iconFrame.measure( MeasureSpec.makeMeasureSpec( child.getLayoutParams().width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); // Ensure that the icon is never larger than the sensor. icon.measure( MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); } else if (child.getId() == R.id.space_above_icon || child.getId() == R.id.space_above_content || child.getId() == R.id.button_bar) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( child.getLayoutParams().height, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.space_below_icon) { // Set the spacer height so the fingerprint icon is on the physical sensor area final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0); child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.description || child.getId() == R.id.customized_view_container) { //skip description view and compute later continue; } else if (child.getId() == R.id.logo) { child.measure( MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } if (child.getVisibility() != View.GONE) { totalHeight += child.getMeasuredHeight(); } } //re-calculate the height of body content View description = mView.findViewById(R.id.description); View contentView = mView.findViewById(R.id.customized_view_container); if (description != null && description.getVisibility() != View.GONE) { totalHeight += measureDescription(description, displayHeight, width, totalHeight); } else if (contentView != null && contentView.getVisibility() != View.GONE) { totalHeight += measureDescription(contentView, displayHeight, width, totalHeight); } return new AuthDialog.LayoutParams(width, totalHeight); } private int measureDescription(View bodyContent, int displayHeight, int currWidth, int currHeight) { int newHeight = bodyContent.getMeasuredHeight() + currHeight; int limit = (int) (displayHeight * 0.75); if (newHeight > limit) { bodyContent.measure( MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY)); } return bodyContent.getMeasuredHeight(); } @NonNull private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height, float scaleFactor) { final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); // Find the spacer height needed to vertically align the icon with the sensor. final int titleHeight = getViewHeightPx(R.id.title); final int subtitleHeight = getViewHeightPx(R.id.subtitle); final int descriptionHeight = getViewHeightPx(R.id.description); final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); final int textIndicatorHeight = getViewHeightPx(R.id.indicator); final int buttonBarHeight = getViewHeightPx(R.id.button_bar); final Insets navbarInsets = getNavbarInsets(windowMetrics); final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, buttonBarHeight, navbarInsets.bottom); // Find the spacer width needed to horizontally align the icon with the sensor. final int displayWidth = getMaximumWindowBounds(windowMetrics).width(); final int dialogMargin = getDialogMarginPx(); final int horizontalInset = navbarInsets.left + navbarInsets.right; final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor); final int sensorDiameter = getSensorDiameter(scaleFactor); final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; int remeasuredHeight = 0; final int numChildren = mView.getChildCount(); for (int i = 0; i < numChildren; i++) { final View child = mView.getChildAt(i); if (child.getId() == R.id.biometric_icon_frame) { final FrameLayout iconFrame = (FrameLayout) child; final View icon = iconFrame.getChildAt(0); // Create a frame that's exactly the height of the sensor circle. iconFrame.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); // Ensure that the icon is never larger than the sensor. icon.measure( MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); } else if (child.getId() == R.id.space_above_icon) { // Adjust the width and height of the top spacer if necessary. final int newTopSpacerHeight = child.getLayoutParams().height - Math.min(bottomSpacerHeight, 0); child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.button_bar) { // Adjust the width of the button bar while preserving its height. child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( child.getLayoutParams().height, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.space_below_icon) { // Adjust the bottom spacer height to align the fingerprint icon with the sensor. final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0); child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY)); } else { // Use the remeasured width for all other child views. child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } if (child.getVisibility() != View.GONE) { remeasuredHeight += child.getMeasuredHeight(); } } return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); } private int getViewHeightPx(@IdRes int viewId) { final View view = mView.findViewById(viewId); return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0; } private int getDialogMarginPx() { return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); } @NonNull private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) { return windowMetrics != null ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars()) : Insets.NONE; } @NonNull private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) { return windowMetrics != null ? windowMetrics.getBounds() : new Rect(); } /** * For devices in portrait orientation where the sensor is too high up, calculates the amount of * padding necessary to center the biometric icon within the sensor's physical location. */ @VisibleForTesting static int calculateBottomSpacerHeightForPortrait( @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, int navbarBottomInsetPx, float scaleFactor) { final SensorLocationInternal location = sensorProperties.getLocation(); final int sensorDistanceFromBottom = displayHeightPx - (int) (scaleFactor * location.sensorLocationY) - (int) (scaleFactor * location.sensorRadius); final int spacerHeight = sensorDistanceFromBottom - textIndicatorHeightPx - buttonBarHeightPx - dialogMarginPx - navbarBottomInsetPx; if (DEBUG) { Log.d(TAG, "Display height: " + displayHeightPx + ", Distance from bottom: " + sensorDistanceFromBottom + ", Bottom margin: " + dialogMarginPx + ", Navbar bottom inset: " + navbarBottomInsetPx + ", Bottom spacer height (portrait): " + spacerHeight + ", Scale Factor: " + scaleFactor); } return spacerHeight; } /** * For devices in landscape orientation where the sensor is too high up, calculates the amount * of padding necessary to center the biometric icon within the sensor's physical location. */ @VisibleForTesting static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, int buttonBarHeightPx, int navbarBottomInsetPx) { final int dialogHeightAboveIcon = titleHeightPx + subtitleHeightPx + descriptionHeightPx + topSpacerHeightPx; final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; final int bottomSpacerHeight = dialogHeightAboveIcon - dialogHeightBelowIcon - navbarBottomInsetPx; if (DEBUG) { Log.d(TAG, "Title height: " + titleHeightPx + ", Subtitle height: " + subtitleHeightPx + ", Description height: " + descriptionHeightPx + ", Top spacer height: " + topSpacerHeightPx + ", Text indicator height: " + textIndicatorHeightPx + ", Button bar height: " + buttonBarHeightPx + ", Navbar bottom inset: " + navbarBottomInsetPx + ", Bottom spacer height (landscape): " + bottomSpacerHeight); } return bottomSpacerHeight; } /** * For devices in landscape orientation where the sensor is too left/right, calculates the * amount of padding necessary to center the biometric icon within the sensor's physical * location. */ @VisibleForTesting static int calculateHorizontalSpacerWidthForLandscape( @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) { final SensorLocationInternal location = sensorProperties.getLocation(); final int sensorDistanceFromEdge = displayWidthPx - (int) (scaleFactor * location.sensorLocationY) - (int) (scaleFactor * location.sensorRadius); final int horizontalPadding = sensorDistanceFromEdge - dialogMarginPx - navbarHorizontalInsetPx; if (DEBUG) { Log.d(TAG, "Display width: " + displayWidthPx + ", Distance from edge: " + sensorDistanceFromEdge + ", Dialog margin: " + dialogMarginPx + ", Navbar horizontal inset: " + navbarHorizontalInsetPx + ", Horizontal spacer width (landscape): " + horizontalPadding + ", Scale Factor: " + scaleFactor); } return horizontalPadding; } }