blob: 43745bf74aae642ffe2eccdbc80c4843de3e6404 [file] [log] [blame]
/*
* 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.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
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.
*/
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.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(
child.getLayoutParams().height, MeasureSpec.EXACTLY));
} else if (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) {
//skip description view and compute later
continue;
} 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 description
View description = mView.findViewById(R.id.description);
if (description != null && description.getVisibility() != View.GONE) {
totalHeight += measureDescription(description, displayHeight, width, totalHeight);
}
return new AuthDialog.LayoutParams(width, totalHeight);
}
private int measureDescription(View description, int displayHeight, int currWidth,
int currHeight) {
//description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
//so we could getMeasuredHeight in onMeasureInternalPortrait directly.
int newHeight = description.getMeasuredHeight() + currHeight;
int limit = (int) (displayHeight * 0.75);
if (newHeight > limit) {
description.measure(
MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
}
return description.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;
}
}