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