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