1 /*
2  * Copyright (C) 2021 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.systemui.biometrics;
18 
19 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
20 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.animation.ObjectAnimator;
26 import android.content.Context;
27 import android.content.res.ColorStateList;
28 import android.graphics.PorterDuff;
29 import android.graphics.PorterDuffColorFilter;
30 import android.graphics.Rect;
31 import android.graphics.RectF;
32 import android.util.AttributeSet;
33 import android.util.MathUtils;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.ImageView;
37 
38 import androidx.annotation.IntDef;
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
42 
43 import com.android.app.animation.Interpolators;
44 import com.android.settingslib.Utils;
45 import com.android.systemui.R;
46 
47 import com.airbnb.lottie.LottieAnimationView;
48 import com.airbnb.lottie.LottieProperty;
49 import com.airbnb.lottie.model.KeyPath;
50 
51 import java.io.PrintWriter;
52 import java.lang.annotation.Retention;
53 import java.lang.annotation.RetentionPolicy;
54 
55 /**
56  * View corresponding with udfps_keyguard_view_legacy.xml
57  */
58 public class UdfpsKeyguardViewLegacy extends UdfpsAnimationView {
59     private UdfpsDrawable mFingerprintDrawable; // placeholder
60     private LottieAnimationView mAodFp;
61     private LottieAnimationView mLockScreenFp;
62 
63     // used when highlighting fp icon:
64     private int mTextColorPrimary;
65     private ImageView mBgProtection;
66     boolean mUdfpsRequested;
67 
68     private AnimatorSet mBackgroundInAnimator = new AnimatorSet();
69     private int mAlpha; // 0-255
70     private float mScaleFactor = 1;
71     private Rect mSensorBounds = new Rect();
72 
73     // AOD anti-burn-in offsets
74     private final int mMaxBurnInOffsetX;
75     private final int mMaxBurnInOffsetY;
76     private float mBurnInOffsetX;
77     private float mBurnInOffsetY;
78     private float mBurnInProgress;
79     private float mInterpolatedDarkAmount;
80     private int mAnimationType = ANIMATION_NONE;
81     private boolean mFullyInflated;
82     private Runnable mOnFinishInflateRunnable;
83 
UdfpsKeyguardViewLegacy(Context context, @Nullable AttributeSet attrs)84     public UdfpsKeyguardViewLegacy(Context context, @Nullable AttributeSet attrs) {
85         super(context, attrs);
86         mFingerprintDrawable = new UdfpsFpDrawable(context);
87 
88         mMaxBurnInOffsetX = context.getResources()
89             .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
90         mMaxBurnInOffsetY = context.getResources()
91             .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
92     }
93 
94     /**
95      * Inflate internal udfps view on a background thread and call the onFinishRunnable
96      * when inflation is finished.
97      */
startIconAsyncInflate(Runnable onFinishInflate)98     public void startIconAsyncInflate(Runnable onFinishInflate) {
99         mOnFinishInflateRunnable = onFinishInflate;
100         // inflate Lottie views on a background thread in case it takes a while to inflate
101         AsyncLayoutInflater inflater = new AsyncLayoutInflater(mContext);
102         inflater.inflate(R.layout.udfps_keyguard_view_internal, this,
103                 mLayoutInflaterFinishListener);
104     }
105 
106     @Override
getDrawable()107     public UdfpsDrawable getDrawable() {
108         return mFingerprintDrawable;
109     }
110 
111     @Override
onSensorRectUpdated(RectF bounds)112     void onSensorRectUpdated(RectF bounds) {
113         super.onSensorRectUpdated(bounds);
114         bounds.round(this.mSensorBounds);
115         postInvalidate();
116     }
117 
118     @Override
onDisplayConfiguring()119     void onDisplayConfiguring() {
120     }
121 
122     @Override
onDisplayUnconfigured()123     void onDisplayUnconfigured() {
124     }
125 
126     @Override
dozeTimeTick()127     public boolean dozeTimeTick() {
128         updateBurnInOffsets();
129         return true;
130     }
131 
updateBurnInOffsets()132     private void updateBurnInOffsets() {
133         if (!mFullyInflated) {
134             return;
135         }
136 
137         // if we're animating from screen off, we can immediately place the icon in the
138         // AoD-burn in location, else we need to translate the icon from LS => AoD.
139         final float darkAmountForAnimation = mAnimationType == ANIMATION_UNLOCKED_SCREEN_OFF
140                 ? 1f : mInterpolatedDarkAmount;
141         mBurnInOffsetX = MathUtils.lerp(0f,
142             getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
143                 - mMaxBurnInOffsetX, darkAmountForAnimation);
144         mBurnInOffsetY = MathUtils.lerp(0f,
145             getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
146                 - mMaxBurnInOffsetY, darkAmountForAnimation);
147         mBurnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(), darkAmountForAnimation);
148 
149         if (mAnimationType == ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN && !mPauseAuth) {
150             mLockScreenFp.setTranslationX(mBurnInOffsetX);
151             mLockScreenFp.setTranslationY(mBurnInOffsetY);
152             mBgProtection.setAlpha(1f - mInterpolatedDarkAmount);
153             mLockScreenFp.setAlpha(1f - mInterpolatedDarkAmount);
154         } else if (darkAmountForAnimation == 0f) {
155             mLockScreenFp.setTranslationX(0);
156             mLockScreenFp.setTranslationY(0);
157             mBgProtection.setAlpha(mAlpha / 255f);
158             mLockScreenFp.setAlpha(mAlpha / 255f);
159         } else {
160             mBgProtection.setAlpha(0f);
161             mLockScreenFp.setAlpha(0f);
162         }
163         mLockScreenFp.setProgress(1f - mInterpolatedDarkAmount);
164 
165         mAodFp.setTranslationX(mBurnInOffsetX);
166         mAodFp.setTranslationY(mBurnInOffsetY);
167         mAodFp.setProgress(mBurnInProgress);
168         mAodFp.setAlpha(mInterpolatedDarkAmount);
169 
170         // done animating
171         final boolean doneAnimatingBetweenAodAndLS =
172                 mAnimationType == ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
173                         && (mInterpolatedDarkAmount == 0f || mInterpolatedDarkAmount == 1f);
174         final boolean doneAnimatingUnlockedScreenOff =
175                 mAnimationType == ANIMATION_UNLOCKED_SCREEN_OFF
176                         && (mInterpolatedDarkAmount == 1f);
177         if (doneAnimatingBetweenAodAndLS || doneAnimatingUnlockedScreenOff) {
178             mAnimationType = ANIMATION_NONE;
179         }
180     }
181 
requestUdfps(boolean request, int color)182     void requestUdfps(boolean request, int color) {
183         mUdfpsRequested = request;
184     }
185 
updateColor()186     void updateColor() {
187         if (!mFullyInflated) {
188             return;
189         }
190 
191         mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext,
192                 com.android.internal.R.attr.materialColorOnSurface);
193         final int backgroundColor = Utils.getColorAttrDefaultColor(getContext(),
194                 com.android.internal.R.attr.materialColorSurfaceContainerHigh);
195         mBgProtection.setImageTintList(ColorStateList.valueOf(backgroundColor));
196         mLockScreenFp.invalidate(); // updated with a valueCallback
197     }
198 
setScaleFactor(float scale)199     void setScaleFactor(float scale) {
200         mScaleFactor = scale;
201     }
202 
updatePadding()203     void updatePadding() {
204         if (mLockScreenFp == null || mAodFp == null) {
205             return;
206         }
207 
208         final int defaultPaddingPx =
209                 getResources().getDimensionPixelSize(R.dimen.lock_icon_padding);
210         final int padding = (int) (defaultPaddingPx * mScaleFactor);
211         mLockScreenFp.setPadding(padding, padding, padding, padding);
212         mAodFp.setPadding(padding, padding, padding, padding);
213     }
214 
215     /**
216      * @param alpha between 0 and 255
217      */
setUnpausedAlpha(int alpha)218     void setUnpausedAlpha(int alpha) {
219         mAlpha = alpha;
220         updateAlpha();
221     }
222 
223     /**
224      * @return alpha between 0 and 255
225      */
getUnpausedAlpha()226     int getUnpausedAlpha() {
227         return mAlpha;
228     }
229 
230     @Override
updateAlpha()231     protected int updateAlpha() {
232         int alpha = super.updateAlpha();
233         updateBurnInOffsets();
234         return alpha;
235     }
236 
237     @Override
calculateAlpha()238     int calculateAlpha() {
239         if (mPauseAuth) {
240             return 0;
241         }
242         return mAlpha;
243     }
244 
245     static final int ANIMATION_NONE = 0;
246     static final int ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN = 1;
247     static final int ANIMATION_UNLOCKED_SCREEN_OFF = 2;
248 
249     @Retention(RetentionPolicy.SOURCE)
250     @IntDef({ANIMATION_NONE, ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN, ANIMATION_UNLOCKED_SCREEN_OFF})
251     private @interface AnimationType {}
252 
onDozeAmountChanged(float linear, float eased, @AnimationType int animationType)253     void onDozeAmountChanged(float linear, float eased, @AnimationType int animationType) {
254         mAnimationType = animationType;
255         mInterpolatedDarkAmount = eased;
256         updateAlpha();
257     }
258 
updateSensorLocation(@onNull Rect sensorBounds)259     void updateSensorLocation(@NonNull Rect sensorBounds) {
260         mSensorBounds.set(sensorBounds);
261     }
262 
263     /**
264      * Animates in the bg protection circle behind the fp icon to highlight the icon.
265      */
animateInUdfpsBouncer(Runnable onEndAnimation)266     void animateInUdfpsBouncer(Runnable onEndAnimation) {
267         if (mBackgroundInAnimator.isRunning() || !mFullyInflated) {
268             // already animating in or not yet inflated
269             return;
270         }
271 
272         // fade in and scale up
273         mBackgroundInAnimator = new AnimatorSet();
274         mBackgroundInAnimator.playTogether(
275                 ObjectAnimator.ofFloat(mBgProtection, View.ALPHA, 0f, 1f),
276                 ObjectAnimator.ofFloat(mBgProtection, View.SCALE_X, 0f, 1f),
277                 ObjectAnimator.ofFloat(mBgProtection, View.SCALE_Y, 0f, 1f));
278         mBackgroundInAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
279         mBackgroundInAnimator.setDuration(500);
280         mBackgroundInAnimator.addListener(new AnimatorListenerAdapter() {
281             @Override
282             public void onAnimationEnd(Animator animation) {
283                 if (onEndAnimation != null) {
284                     onEndAnimation.run();
285                 }
286             }
287         });
288         mBackgroundInAnimator.start();
289     }
290 
291     /**
292      * Print debugging information for this class.
293      */
dump(PrintWriter pw)294     public void dump(PrintWriter pw) {
295         pw.println("UdfpsKeyguardView (" + this + ")");
296         pw.println("    mPauseAuth=" + mPauseAuth);
297         pw.println("    mUnpausedAlpha=" + getUnpausedAlpha());
298         pw.println("    mUdfpsRequested=" + mUdfpsRequested);
299         pw.println("    mInterpolatedDarkAmount=" + mInterpolatedDarkAmount);
300         pw.println("    mAnimationType=" + mAnimationType);
301         pw.println("    mUseExpandedOverlay=" + mUseExpandedOverlay);
302     }
303 
304     private final AsyncLayoutInflater.OnInflateFinishedListener mLayoutInflaterFinishListener =
305             new AsyncLayoutInflater.OnInflateFinishedListener() {
306         @Override
307         public void onInflateFinished(View view, int resid, ViewGroup parent) {
308             mFullyInflated = true;
309             mAodFp = view.findViewById(R.id.udfps_aod_fp);
310             mLockScreenFp = view.findViewById(R.id.udfps_lockscreen_fp);
311             mBgProtection = view.findViewById(R.id.udfps_keyguard_fp_bg);
312 
313             updatePadding();
314             updateColor();
315             updateAlpha();
316 
317             if (mUseExpandedOverlay) {
318                 final LayoutParams lp = (LayoutParams) view.getLayoutParams();
319                 lp.width = mSensorBounds.width();
320                 lp.height = mSensorBounds.height();
321                 RectF relativeToView = getBoundsRelativeToView(new RectF(mSensorBounds));
322                 lp.setMarginsRelative(
323                         (int) relativeToView.left,
324                         (int) relativeToView.top,
325                         (int) relativeToView.right,
326                         (int) relativeToView.bottom
327                 );
328                 parent.addView(view, lp);
329             } else {
330                 parent.addView(view);
331             }
332 
333             // requires call to invalidate to update the color
334             mLockScreenFp.addValueCallback(
335                     new KeyPath("**"), LottieProperty.COLOR_FILTER,
336                     frameInfo -> new PorterDuffColorFilter(mTextColorPrimary,
337                             PorterDuff.Mode.SRC_ATOP)
338             );
339             mOnFinishInflateRunnable.run();
340         }
341     };
342 }
343