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