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.animation.Animator; 20 import android.animation.ObjectAnimator; 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.content.res.TypedArray; 24 import android.graphics.Insets; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 28 import android.os.Handler; 29 import android.text.TextUtils; 30 import android.util.AttributeSet; 31 import android.util.DisplayMetrics; 32 import android.util.TypedValue; 33 import android.view.Display; 34 import android.view.DisplayInfo; 35 import android.view.Gravity; 36 import android.view.LayoutInflater; 37 import android.view.Surface; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.WindowInsets; 41 import android.view.WindowManager; 42 import android.view.accessibility.AccessibilityManager; 43 import android.widget.Button; 44 import android.widget.FrameLayout; 45 import android.widget.ImageView; 46 import android.widget.LinearLayout; 47 import android.widget.ScrollView; 48 import android.widget.TextView; 49 50 import androidx.annotation.ColorInt; 51 import androidx.annotation.LayoutRes; 52 import androidx.annotation.NonNull; 53 import androidx.annotation.VisibleForTesting; 54 55 import com.android.settings.R; 56 import com.android.settings.flags.Flags; 57 import com.android.systemui.biometrics.UdfpsUtils; 58 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; 59 60 import com.google.android.setupcompat.template.FooterBarMixin; 61 import com.google.android.setupdesign.GlifLayout; 62 import com.google.android.setupdesign.view.BottomScrollView; 63 64 import java.util.Locale; 65 66 /** 67 * View for udfps enrolling. 68 */ 69 public class UdfpsEnrollEnrollingView extends GlifLayout { 70 71 private final UdfpsUtils mUdfpsUtils; 72 private final Context mContext; 73 // We don't need to listen to onConfigurationChanged() for mRotation here because 74 // FingerprintEnrollEnrolling is always recreated once the configuration is changed. 75 private final int mRotation; 76 private final boolean mIsLandscape; 77 private final boolean mShouldUseReverseLandscape; 78 79 private WindowManager mWindowManager; 80 81 private UdfpsEnrollView mUdfpsEnrollView; 82 private View mHeaderView; 83 private AccessibilityManager mAccessibilityManager; 84 85 private ObjectAnimator mHeaderScrollAnimator; 86 UdfpsEnrollEnrollingView(Context context, AttributeSet attrs)87 public UdfpsEnrollEnrollingView(Context context, AttributeSet attrs) { 88 super(context, attrs); 89 mContext = context; 90 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 91 mRotation = mContext.getDisplay().getRotation(); 92 mIsLandscape = mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; 93 final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) 94 == View.LAYOUT_DIRECTION_RTL); 95 mShouldUseReverseLandscape = (mRotation == Surface.ROTATION_90 && isLayoutRtl) 96 || (mRotation == Surface.ROTATION_270 && !isLayoutRtl); 97 98 mUdfpsUtils = new UdfpsUtils(); 99 } 100 101 @Override onFinishInflate()102 protected void onFinishInflate() { 103 super.onFinishInflate(); 104 mHeaderView = findViewById(com.google.android.setupdesign.R.id.sud_landscape_header_area); 105 mUdfpsEnrollView = findViewById(R.id.udfps_animation_view); 106 } 107 108 @Override onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)109 protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) { 110 final Configuration config = inflater.getContext().getResources().getConfiguration(); 111 if (Flags.enrollLayoutTruncateImprovement() 112 && config.orientation == Configuration.ORIENTATION_PORTRAIT) { 113 template = R.layout.biometrics_glif_compact; 114 } 115 return super.onInflateTemplate(inflater, template); 116 } 117 setDecreasePadding(int decreasePadding)118 void setDecreasePadding(int decreasePadding) { 119 if (mUdfpsEnrollView != null) { 120 mUdfpsEnrollView.setDecreasePadding(decreasePadding); 121 } 122 } 123 onUdfpsSensorRectUpdated()124 void onUdfpsSensorRectUpdated() { 125 if (mUdfpsEnrollView != null) { 126 mUdfpsEnrollView.setVisibility(VISIBLE); 127 } 128 } 129 getScrollableGlifHeaderHeight(boolean isShouldShowLottie)130 private int getScrollableGlifHeaderHeight(boolean isShouldShowLottie) { 131 final TypedValue tvRatio = new TypedValue(); 132 if (isLargeDisplaySizeOrFontSize() && !isShouldShowLottie) { 133 getResources().getValue( 134 R.dimen.biometrics_glif_header_height_ratio_large, tvRatio, true); 135 } else { 136 getResources().getValue(R.dimen.biometrics_glif_header_height_ratio, tvRatio, true); 137 } 138 final float newHeaderHeight = (float) getResources().getDisplayMetrics().heightPixels 139 * tvRatio.getFloat(); 140 141 return (int) newHeaderHeight; 142 } 143 adjustScrollableHeaderHeight(ScrollView headerScrollView, boolean isShouldShowLottie)144 void adjustScrollableHeaderHeight(ScrollView headerScrollView, boolean isShouldShowLottie) { 145 ViewGroup.LayoutParams params = headerScrollView.getLayoutParams(); 146 params.height = getScrollableGlifHeaderHeight(isShouldShowLottie); 147 headerScrollView.setLayoutParams(params); 148 } 149 isLargeDisplaySizeOrFontSize()150 private boolean isLargeDisplaySizeOrFontSize() { 151 final Configuration config = getResources().getConfiguration(); 152 if (config.fontScale > 1.3f || getLargeDisplayScale() >= 2.8f) { 153 return true; 154 } 155 return false; 156 } 157 getLargeDisplayScale()158 private float getLargeDisplayScale() { 159 final Display display = mWindowManager.getDefaultDisplay(); 160 final DisplayMetrics metrics = new DisplayMetrics(); 161 display.getMetrics(metrics); 162 return metrics.scaledDensity; 163 } 164 adjustUdfpsVieWithFooterBar()165 void adjustUdfpsVieWithFooterBar() { 166 final FrameLayout allContent = findViewById(R.id.suc_layout_status); 167 final ImageView udfpsProgressView = findViewById( 168 R.id.udfps_enroll_animation_fp_progress_view); 169 170 final int navigationBarHeight = getNaviBarHeight(); 171 final int footerBarHeight = getFooterBarHeight(); 172 173 final int udfpsProgressDrawableBottom = getOnScreenPositionTop(udfpsProgressView) 174 + udfpsProgressView.getDrawable().getBounds().height() 175 - udfpsProgressView.getPaddingBottom() + 2 /* reserved for more space */; 176 final int footerBarTop = getOnScreenPositionTop(allContent) + allContent.getHeight() 177 - (footerBarHeight + navigationBarHeight); 178 179 if (udfpsProgressDrawableBottom > footerBarTop) { 180 int adjustPadding = udfpsProgressDrawableBottom - footerBarTop; 181 setDecreasePadding(adjustPadding); 182 } 183 } 184 getOnScreenPositionTop(View view)185 private int getOnScreenPositionTop(View view) { 186 int [] location = new int[2]; 187 view.getLocationOnScreen(location); 188 return location[1]; 189 } 190 getNaviBarHeight()191 private int getNaviBarHeight() { 192 final Insets inset = mWindowManager.getMaximumWindowMetrics().getWindowInsets().getInsets( 193 WindowInsets.Type.navigationBars()); 194 return inset.toRect().height(); 195 } 196 getFooterBarHeight()197 private int getFooterBarHeight() { 198 TypedArray a = mContext.getTheme().obtainStyledAttributes(new int[] { 199 com.google.android.setupcompat.R.attr.sucFooterBarMinHeight}); 200 final int footerBarMinHeight = a.getDimensionPixelSize(0, -1); 201 a.recycle(); 202 return footerBarMinHeight; 203 } 204 setFocusOnDescription()205 void setFocusOnDescription() { 206 final ScrollView headerScrollView = findViewById(R.id.sud_header_scroll_view); 207 final TextView descriptionView = getDescriptionTextView(); 208 if (descriptionView != null && !descriptionView.getText().isEmpty()) { 209 descriptionView.post( 210 () -> { 211 Rect scrollBounds = new Rect(); 212 headerScrollView.getHitRect(scrollBounds); 213 boolean isVisible = descriptionView.getLocalVisibleRect(scrollBounds); 214 if (!isVisible) { 215 descriptionView.setFocusable(true); 216 descriptionView.setFocusableInTouchMode(true); 217 descriptionView.requestFocus(); 218 } 219 }); 220 } 221 } 222 headerVerticalScrolling(ScrollView headerScrollView, long duration, boolean isAccessibilityEnabled)223 void headerVerticalScrolling(ScrollView headerScrollView, long duration, 224 boolean isAccessibilityEnabled) { 225 headerScrollView.post(new Runnable() { 226 @Override 227 public void run() { 228 final int maxScroll = headerScrollView.getChildAt(0).getMeasuredHeight() 229 - headerScrollView.getMeasuredHeight(); 230 mHeaderScrollAnimator = ObjectAnimator.ofInt( 231 headerScrollView, "scrollY", maxScroll); 232 mHeaderScrollAnimator.setDuration(duration); 233 mHeaderScrollAnimator.addListener(new Animator.AnimatorListener() { 234 235 @Override 236 public void onAnimationStart(@NonNull Animator animation) {} 237 238 @Override 239 public void onAnimationEnd(@NonNull Animator animation) { 240 headerScrollView.post(new Runnable() { 241 @Override 242 public void run() { 243 mHeaderScrollAnimator.removeAllListeners(); 244 mHeaderScrollAnimator.reverse(); 245 if (isAccessibilityEnabled) { 246 new Handler().postDelayed(new Runnable() { 247 @Override 248 public void run() { 249 if (!mHeaderScrollAnimator.isRunning()) { 250 setFocusOnDescription(); 251 } 252 } 253 }, duration + 200); 254 } 255 } 256 }); 257 } 258 259 @Override 260 public void onAnimationCancel(@NonNull Animator animation) {} 261 262 @Override 263 public void onAnimationRepeat(@NonNull Animator animation) {} 264 }); 265 mHeaderScrollAnimator.start(); 266 } 267 }); 268 } 269 initView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper, AccessibilityManager accessibilityManager)270 void initView(FingerprintSensorPropertiesInternal udfpsProps, 271 UdfpsEnrollHelper udfpsEnrollHelper, 272 AccessibilityManager accessibilityManager) { 273 mAccessibilityManager = accessibilityManager; 274 initUdfpsEnrollView(udfpsProps, udfpsEnrollHelper); 275 276 if (!mIsLandscape) { 277 adjustPortraitPaddings(); 278 } else if (mShouldUseReverseLandscape) { 279 swapHeaderAndContent(); 280 } 281 mUdfpsEnrollView.setVisibility(View.INVISIBLE); 282 setOnHoverListener(); 283 } 284 setSecondaryButtonBackground(@olorInt int color)285 void setSecondaryButtonBackground(@ColorInt int color) { 286 // Set the button background only when the button is not under udfps overlay to avoid UI 287 // overlap. 288 if (!mIsLandscape || mShouldUseReverseLandscape) { 289 return; 290 } 291 final Button secondaryButtonView = 292 getMixin(FooterBarMixin.class).getSecondaryButtonView(); 293 secondaryButtonView.setBackgroundColor(color); 294 if (mRotation == Surface.ROTATION_90) { 295 secondaryButtonView.setGravity(Gravity.START); 296 } else { 297 secondaryButtonView.setGravity(Gravity.END); 298 } 299 mHeaderView.post(() -> { 300 secondaryButtonView.setLayoutParams( 301 new LinearLayout.LayoutParams(mHeaderView.getMeasuredWidth(), 302 ViewGroup.LayoutParams.WRAP_CONTENT)); 303 }); 304 } 305 initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps, UdfpsEnrollHelper udfpsEnrollHelper)306 private void initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps, 307 UdfpsEnrollHelper udfpsEnrollHelper) { 308 DisplayInfo displayInfo = new DisplayInfo(); 309 mContext.getDisplay().getDisplayInfo(displayInfo); 310 311 final float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo); 312 Rect udfpsBounds = udfpsProps.getLocation().getRect(); 313 udfpsBounds.scale(scaleFactor); 314 315 final Rect overlayBounds = new Rect( 316 0, /* left */ 317 displayInfo.getNaturalHeight() / 2, /* top */ 318 displayInfo.getNaturalWidth(), /* right */ 319 displayInfo.getNaturalHeight() /* botom */); 320 321 UdfpsOverlayParams params = new UdfpsOverlayParams( 322 udfpsBounds, 323 overlayBounds, 324 displayInfo.getNaturalWidth(), 325 displayInfo.getNaturalHeight(), 326 scaleFactor, 327 displayInfo.rotation, 328 udfpsProps.sensorType); 329 330 mUdfpsEnrollView.setOverlayParams(params); 331 mUdfpsEnrollView.setEnrollHelper(udfpsEnrollHelper); 332 } 333 adjustPortraitPaddings()334 private void adjustPortraitPaddings() { 335 // In the portrait mode, layout_container's height is 0, so it's 336 // always shown at the bottom of the screen. 337 final FrameLayout portraitLayoutContainer = findViewById(R.id.layout_container); 338 339 // In the portrait mode, the title and lottie animation view may 340 // overlap when title needs three lines, so adding some paddings 341 // between them, and adjusting the fp progress view here accordingly. 342 final int layoutLottieAnimationPadding = (int) getResources() 343 .getDimension(R.dimen.udfps_lottie_padding_top); 344 portraitLayoutContainer.setPadding(0, 345 layoutLottieAnimationPadding, 0, 0); 346 final ImageView progressView = mUdfpsEnrollView.findViewById( 347 R.id.udfps_enroll_animation_fp_progress_view); 348 progressView.setPadding(0, -(layoutLottieAnimationPadding), 349 0, layoutLottieAnimationPadding); 350 final ImageView fingerprintView = mUdfpsEnrollView.findViewById( 351 R.id.udfps_enroll_animation_fp_view); 352 fingerprintView.setPadding(0, -layoutLottieAnimationPadding, 353 0, layoutLottieAnimationPadding); 354 355 // TODO(b/260970216) Instead of hiding the description text view, we should 356 // make the header view scrollable if the text is too long. 357 // If description text view has overlap with udfps progress view, hide it. 358 if (!Flags.enrollLayoutTruncateImprovement()) { 359 final View descView = getDescriptionTextView(); 360 getViewTreeObserver().addOnDrawListener(() -> { 361 if (descView.getVisibility() == View.VISIBLE 362 && hasOverlap(descView, mUdfpsEnrollView)) { 363 descView.setVisibility(View.GONE); 364 } 365 }); 366 } 367 } 368 setOnHoverListener()369 private void setOnHoverListener() { 370 if (!mAccessibilityManager.isEnabled()) return; 371 372 final View.OnHoverListener onHoverListener = (v, event) -> { 373 // Map the touch to portrait mode if the device is in 374 // landscape mode. 375 final Point scaledTouch = 376 mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), 377 event, mUdfpsEnrollView.getOverlayParams()); 378 379 if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event, 380 mUdfpsEnrollView.getOverlayParams())) { 381 return false; 382 } 383 384 final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea( 385 mAccessibilityManager.isTouchExplorationEnabled(), mContext, 386 scaledTouch.x, scaledTouch.y, mUdfpsEnrollView.getOverlayParams()); 387 if (theStr != null) { 388 v.announceForAccessibility(theStr); 389 } 390 return false; 391 }; 392 393 findManagedViewById(mIsLandscape 394 ? com.google.android.setupdesign.R.id.sud_landscape_content_area 395 : com.google.android.setupdesign.R.id.sud_layout_content 396 ).setOnHoverListener(onHoverListener); 397 } 398 swapHeaderAndContent()399 private void swapHeaderAndContent() { 400 // Reverse header and body 401 ViewGroup parentView = (ViewGroup) mHeaderView.getParent(); 402 parentView.removeView(mHeaderView); 403 parentView.addView(mHeaderView); 404 405 // Hide scroll indicators 406 BottomScrollView headerScrollView = mHeaderView.findViewById( 407 com.google.android.setupdesign.R.id.sud_header_scroll_view); 408 headerScrollView.setScrollIndicators(0); 409 } 410 411 @VisibleForTesting hasOverlap(View view1, View view2)412 boolean hasOverlap(View view1, View view2) { 413 int[] firstPosition = new int[2]; 414 int[] secondPosition = new int[2]; 415 416 view1.getLocationOnScreen(firstPosition); 417 view2.getLocationOnScreen(secondPosition); 418 419 // Rect constructor parameters: left, top, right, bottom 420 Rect rectView1 = new Rect(firstPosition[0], firstPosition[1], 421 firstPosition[0] + view1.getMeasuredWidth(), 422 firstPosition[1] + view1.getMeasuredHeight()); 423 Rect rectView2 = new Rect(secondPosition[0], secondPosition[1], 424 secondPosition[0] + view2.getMeasuredWidth(), 425 secondPosition[1] + view2.getMeasuredHeight()); 426 return rectView1.intersect(rectView2); 427 } 428 } 429