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