• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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