/* * Copyright (C) 2023 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.settings.biometrics.fingerprint; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.text.TextUtils; import android.util.AttributeSet; import android.view.DisplayInfo; import android.view.Gravity; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import androidx.annotation.ColorInt; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.systemui.biometrics.UdfpsUtils; import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; import com.google.android.setupcompat.template.FooterBarMixin; import com.google.android.setupdesign.GlifLayout; import com.google.android.setupdesign.view.BottomScrollView; import java.util.Locale; /** * View for udfps enrolling. */ public class UdfpsEnrollEnrollingView extends GlifLayout { private final UdfpsUtils mUdfpsUtils; private final Context mContext; // We don't need to listen to onConfigurationChanged() for mRotation here because // FingerprintEnrollEnrolling is always recreated once the configuration is changed. private final int mRotation; private final boolean mIsLandscape; private final boolean mShouldUseReverseLandscape; private UdfpsEnrollView mUdfpsEnrollView; private View mHeaderView; private AccessibilityManager mAccessibilityManager; public UdfpsEnrollEnrollingView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; mRotation = mContext.getDisplay().getRotation(); mIsLandscape = mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL); mShouldUseReverseLandscape = (mRotation == Surface.ROTATION_90 && isLayoutRtl) || (mRotation == Surface.ROTATION_270 && !isLayoutRtl); mUdfpsUtils = new UdfpsUtils(); } @Override protected void onFinishInflate() { super.onFinishInflate(); mHeaderView = findViewById(com.google.android.setupdesign.R.id.sud_landscape_header_area); mUdfpsEnrollView = findViewById(R.id.udfps_animation_view); } void initView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper, AccessibilityManager accessibilityManager) { mAccessibilityManager = accessibilityManager; initUdfpsEnrollView(udfpsProps, udfpsEnrollHelper); if (!mIsLandscape) { adjustPortraitPaddings(); } else if (mShouldUseReverseLandscape) { swapHeaderAndContent(); } setOnHoverListener(); } void setSecondaryButtonBackground(@ColorInt int color) { // Set the button background only when the button is not under udfps overlay to avoid UI // overlap. if (!mIsLandscape || mShouldUseReverseLandscape) { return; } final Button secondaryButtonView = getMixin(FooterBarMixin.class).getSecondaryButtonView(); secondaryButtonView.setBackgroundColor(color); if (mRotation == Surface.ROTATION_90) { secondaryButtonView.setGravity(Gravity.START); } else { secondaryButtonView.setGravity(Gravity.END); } mHeaderView.post(() -> { secondaryButtonView.setLayoutParams( new LinearLayout.LayoutParams(mHeaderView.getMeasuredWidth(), ViewGroup.LayoutParams.WRAP_CONTENT)); }); } private void initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper) { DisplayInfo displayInfo = new DisplayInfo(); mContext.getDisplay().getDisplayInfo(displayInfo); final float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo); Rect udfpsBounds = udfpsProps.getLocation().getRect(); udfpsBounds.scale(scaleFactor); final Rect overlayBounds = new Rect( 0, /* left */ displayInfo.getNaturalHeight() / 2, /* top */ displayInfo.getNaturalWidth(), /* right */ displayInfo.getNaturalHeight() /* botom */); UdfpsOverlayParams params = new UdfpsOverlayParams( udfpsBounds, overlayBounds, displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(), scaleFactor, displayInfo.rotation, udfpsProps.sensorType); mUdfpsEnrollView.setOverlayParams(params); mUdfpsEnrollView.setEnrollHelper(udfpsEnrollHelper); } private void adjustPortraitPaddings() { // In the portrait mode, layout_container's height is 0, so it's // always shown at the bottom of the screen. final FrameLayout portraitLayoutContainer = findViewById(R.id.layout_container); // In the portrait mode, the title and lottie animation view may // overlap when title needs three lines, so adding some paddings // between them, and adjusting the fp progress view here accordingly. final int layoutLottieAnimationPadding = (int) getResources() .getDimension(R.dimen.udfps_lottie_padding_top); portraitLayoutContainer.setPadding(0, layoutLottieAnimationPadding, 0, 0); final ImageView progressView = mUdfpsEnrollView.findViewById( R.id.udfps_enroll_animation_fp_progress_view); progressView.setPadding(0, -(layoutLottieAnimationPadding), 0, layoutLottieAnimationPadding); final ImageView fingerprintView = mUdfpsEnrollView.findViewById( R.id.udfps_enroll_animation_fp_view); fingerprintView.setPadding(0, -layoutLottieAnimationPadding, 0, layoutLottieAnimationPadding); // TODO(b/260970216) Instead of hiding the description text view, we should // make the header view scrollable if the text is too long. // If description text view has overlap with udfps progress view, hide it. final View descView = getDescriptionTextView(); getViewTreeObserver().addOnDrawListener(() -> { if (descView.getVisibility() == View.VISIBLE && hasOverlap(descView, mUdfpsEnrollView)) { descView.setVisibility(View.GONE); } }); } private void setOnHoverListener() { if (!mAccessibilityManager.isEnabled()) return; final View.OnHoverListener onHoverListener = (v, event) -> { // Map the touch to portrait mode if the device is in // landscape mode. final Point scaledTouch = mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, mUdfpsEnrollView.getOverlayParams()); if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event, mUdfpsEnrollView.getOverlayParams())) { return false; } final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea( mAccessibilityManager.isTouchExplorationEnabled(), mContext, scaledTouch.x, scaledTouch.y, mUdfpsEnrollView.getOverlayParams()); if (theStr != null) { v.announceForAccessibility(theStr); } return false; }; findManagedViewById(mIsLandscape ? com.google.android.setupdesign.R.id.sud_landscape_content_area : com.google.android.setupdesign.R.id.sud_layout_content ).setOnHoverListener(onHoverListener); } private void swapHeaderAndContent() { // Reverse header and body ViewGroup parentView = (ViewGroup) mHeaderView.getParent(); parentView.removeView(mHeaderView); parentView.addView(mHeaderView); // Hide scroll indicators BottomScrollView headerScrollView = mHeaderView.findViewById( com.google.android.setupdesign.R.id.sud_header_scroll_view); headerScrollView.setScrollIndicators(0); } @VisibleForTesting boolean hasOverlap(View view1, View view2) { int[] firstPosition = new int[2]; int[] secondPosition = new int[2]; view1.getLocationOnScreen(firstPosition); view2.getLocationOnScreen(secondPosition); // Rect constructor parameters: left, top, right, bottom Rect rectView1 = new Rect(firstPosition[0], firstPosition[1], firstPosition[0] + view1.getMeasuredWidth(), firstPosition[1] + view1.getMeasuredHeight()); Rect rectView2 = new Rect(secondPosition[0], secondPosition[1], secondPosition[0] + view2.getMeasuredWidth(), secondPosition[1] + view2.getMeasuredHeight()); return rectView1.intersect(rectView2); } }