1 /* 2 * Copyright (C) 2023 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.settings.biometrics.fingerprint; 18 19 import android.content.Context; 20 import android.graphics.Point; 21 import android.graphics.Rect; 22 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 23 import android.text.TextUtils; 24 import android.util.AttributeSet; 25 import android.view.DisplayInfo; 26 import android.view.Gravity; 27 import android.view.Surface; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.accessibility.AccessibilityManager; 31 import android.widget.Button; 32 import android.widget.FrameLayout; 33 import android.widget.ImageView; 34 import android.widget.LinearLayout; 35 36 import androidx.annotation.ColorInt; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.settings.R; 40 import com.android.systemui.biometrics.UdfpsUtils; 41 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; 42 43 import com.google.android.setupcompat.template.FooterBarMixin; 44 import com.google.android.setupdesign.GlifLayout; 45 import com.google.android.setupdesign.view.BottomScrollView; 46 47 import java.util.Locale; 48 49 /** 50 * View for udfps enrolling. 51 */ 52 public class UdfpsEnrollEnrollingView extends GlifLayout { 53 private final UdfpsUtils mUdfpsUtils; 54 private final Context mContext; 55 // We don't need to listen to onConfigurationChanged() for mRotation here because 56 // FingerprintEnrollEnrolling is always recreated once the configuration is changed. 57 private final int mRotation; 58 private final boolean mIsLandscape; 59 private final boolean mShouldUseReverseLandscape; 60 private UdfpsEnrollView mUdfpsEnrollView; 61 private View mHeaderView; 62 private AccessibilityManager mAccessibilityManager; 63 64 UdfpsEnrollEnrollingView(Context context, AttributeSet attrs)65 public UdfpsEnrollEnrollingView(Context context, AttributeSet attrs) { 66 super(context, attrs); 67 mContext = context; 68 mRotation = mContext.getDisplay().getRotation(); 69 mIsLandscape = mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; 70 final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) 71 == View.LAYOUT_DIRECTION_RTL); 72 mShouldUseReverseLandscape = (mRotation == Surface.ROTATION_90 && isLayoutRtl) 73 || (mRotation == Surface.ROTATION_270 && !isLayoutRtl); 74 75 mUdfpsUtils = new UdfpsUtils(); 76 } 77 78 @Override onFinishInflate()79 protected void onFinishInflate() { 80 super.onFinishInflate(); 81 mHeaderView = findViewById(com.google.android.setupdesign.R.id.sud_landscape_header_area); 82 mUdfpsEnrollView = findViewById(R.id.udfps_animation_view); 83 } 84 initView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper, AccessibilityManager accessibilityManager)85 void initView(FingerprintSensorPropertiesInternal udfpsProps, 86 UdfpsEnrollHelper udfpsEnrollHelper, 87 AccessibilityManager accessibilityManager) { 88 mAccessibilityManager = accessibilityManager; 89 initUdfpsEnrollView(udfpsProps, udfpsEnrollHelper); 90 91 if (!mIsLandscape) { 92 adjustPortraitPaddings(); 93 } else if (mShouldUseReverseLandscape) { 94 swapHeaderAndContent(); 95 } 96 setOnHoverListener(); 97 } 98 setSecondaryButtonBackground(@olorInt int color)99 void setSecondaryButtonBackground(@ColorInt int color) { 100 // Set the button background only when the button is not under udfps overlay to avoid UI 101 // overlap. 102 if (!mIsLandscape || mShouldUseReverseLandscape) { 103 return; 104 } 105 final Button secondaryButtonView = 106 getMixin(FooterBarMixin.class).getSecondaryButtonView(); 107 secondaryButtonView.setBackgroundColor(color); 108 if (mRotation == Surface.ROTATION_90) { 109 secondaryButtonView.setGravity(Gravity.START); 110 } else { 111 secondaryButtonView.setGravity(Gravity.END); 112 } 113 mHeaderView.post(() -> { 114 secondaryButtonView.setLayoutParams( 115 new LinearLayout.LayoutParams(mHeaderView.getMeasuredWidth(), 116 ViewGroup.LayoutParams.WRAP_CONTENT)); 117 }); 118 } 119 initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper)120 private void initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps, 121 UdfpsEnrollHelper udfpsEnrollHelper) { 122 DisplayInfo displayInfo = new DisplayInfo(); 123 mContext.getDisplay().getDisplayInfo(displayInfo); 124 125 final float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo); 126 Rect udfpsBounds = udfpsProps.getLocation().getRect(); 127 udfpsBounds.scale(scaleFactor); 128 129 final Rect overlayBounds = new Rect( 130 0, /* left */ 131 displayInfo.getNaturalHeight() / 2, /* top */ 132 displayInfo.getNaturalWidth(), /* right */ 133 displayInfo.getNaturalHeight() /* botom */); 134 135 UdfpsOverlayParams params = new UdfpsOverlayParams( 136 udfpsBounds, 137 overlayBounds, 138 displayInfo.getNaturalWidth(), 139 displayInfo.getNaturalHeight(), 140 scaleFactor, 141 displayInfo.rotation, 142 udfpsProps.sensorType); 143 144 mUdfpsEnrollView.setOverlayParams(params); 145 mUdfpsEnrollView.setEnrollHelper(udfpsEnrollHelper); 146 } 147 adjustPortraitPaddings()148 private void adjustPortraitPaddings() { 149 // In the portrait mode, layout_container's height is 0, so it's 150 // always shown at the bottom of the screen. 151 final FrameLayout portraitLayoutContainer = findViewById(R.id.layout_container); 152 153 // In the portrait mode, the title and lottie animation view may 154 // overlap when title needs three lines, so adding some paddings 155 // between them, and adjusting the fp progress view here accordingly. 156 final int layoutLottieAnimationPadding = (int) getResources() 157 .getDimension(R.dimen.udfps_lottie_padding_top); 158 portraitLayoutContainer.setPadding(0, 159 layoutLottieAnimationPadding, 0, 0); 160 final ImageView progressView = mUdfpsEnrollView.findViewById( 161 R.id.udfps_enroll_animation_fp_progress_view); 162 progressView.setPadding(0, -(layoutLottieAnimationPadding), 163 0, layoutLottieAnimationPadding); 164 final ImageView fingerprintView = mUdfpsEnrollView.findViewById( 165 R.id.udfps_enroll_animation_fp_view); 166 fingerprintView.setPadding(0, -layoutLottieAnimationPadding, 167 0, layoutLottieAnimationPadding); 168 169 // TODO(b/260970216) Instead of hiding the description text view, we should 170 // make the header view scrollable if the text is too long. 171 // If description text view has overlap with udfps progress view, hide it. 172 final View descView = getDescriptionTextView(); 173 getViewTreeObserver().addOnDrawListener(() -> { 174 if (descView.getVisibility() == View.VISIBLE 175 && hasOverlap(descView, mUdfpsEnrollView)) { 176 descView.setVisibility(View.GONE); 177 } 178 }); 179 } 180 setOnHoverListener()181 private void setOnHoverListener() { 182 if (!mAccessibilityManager.isEnabled()) return; 183 184 final View.OnHoverListener onHoverListener = (v, event) -> { 185 // Map the touch to portrait mode if the device is in 186 // landscape mode. 187 final Point scaledTouch = 188 mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), 189 event, mUdfpsEnrollView.getOverlayParams()); 190 191 if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event, 192 mUdfpsEnrollView.getOverlayParams())) { 193 return false; 194 } 195 196 final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea( 197 mAccessibilityManager.isTouchExplorationEnabled(), mContext, 198 scaledTouch.x, scaledTouch.y, mUdfpsEnrollView.getOverlayParams()); 199 if (theStr != null) { 200 v.announceForAccessibility(theStr); 201 } 202 return false; 203 }; 204 205 findManagedViewById(mIsLandscape 206 ? com.google.android.setupdesign.R.id.sud_landscape_content_area 207 : com.google.android.setupdesign.R.id.sud_layout_content 208 ).setOnHoverListener(onHoverListener); 209 } 210 swapHeaderAndContent()211 private void swapHeaderAndContent() { 212 // Reverse header and body 213 ViewGroup parentView = (ViewGroup) mHeaderView.getParent(); 214 parentView.removeView(mHeaderView); 215 parentView.addView(mHeaderView); 216 217 // Hide scroll indicators 218 BottomScrollView headerScrollView = mHeaderView.findViewById( 219 com.google.android.setupdesign.R.id.sud_header_scroll_view); 220 headerScrollView.setScrollIndicators(0); 221 } 222 223 @VisibleForTesting hasOverlap(View view1, View view2)224 boolean hasOverlap(View view1, View view2) { 225 int[] firstPosition = new int[2]; 226 int[] secondPosition = new int[2]; 227 228 view1.getLocationOnScreen(firstPosition); 229 view2.getLocationOnScreen(secondPosition); 230 231 // Rect constructor parameters: left, top, right, bottom 232 Rect rectView1 = new Rect(firstPosition[0], firstPosition[1], 233 firstPosition[0] + view1.getMeasuredWidth(), 234 firstPosition[1] + view1.getMeasuredHeight()); 235 Rect rectView2 = new Rect(secondPosition[0], secondPosition[1], 236 secondPosition[0] + view2.getMeasuredWidth(), 237 secondPosition[1] + view2.getMeasuredHeight()); 238 return rectView1.intersect(rectView2); 239 } 240 } 241