| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.systemui.statusbar.phone; |
| |
| import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; |
| |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.content.res.Configuration; |
| import android.content.res.TypedArray; |
| import android.graphics.Color; |
| import android.graphics.drawable.Animatable2; |
| import android.graphics.drawable.AnimatedVectorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.hardware.biometrics.BiometricSourceType; |
| import android.util.AttributeSet; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| import com.android.internal.graphics.ColorUtils; |
| import com.android.keyguard.KeyguardUpdateMonitor; |
| import com.android.keyguard.KeyguardUpdateMonitorCallback; |
| import com.android.systemui.R; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.KeyguardAffordanceView; |
| import com.android.systemui.statusbar.policy.AccessibilityController; |
| import com.android.systemui.statusbar.policy.ConfigurationController; |
| import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; |
| |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| |
| /** |
| * Manages the different states and animations of the unlock icon. |
| */ |
| public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener, |
| StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener, |
| UnlockMethodCache.OnUnlockMethodChangedListener { |
| |
| private static final int FP_DRAW_OFF_TIMEOUT = 800; |
| |
| private static final int STATE_LOCKED = 0; |
| private static final int STATE_LOCK_OPEN = 1; |
| private static final int STATE_SCANNING_FACE = 2; |
| private static final int STATE_BIOMETRICS_ERROR = 3; |
| private final ConfigurationController mConfigurationController; |
| private final StatusBarStateController mStatusBarStateController; |
| private final UnlockMethodCache mUnlockMethodCache; |
| private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| private final AccessibilityController mAccessibilityController; |
| |
| private int mLastState = 0; |
| private boolean mTransientBiometricsError; |
| private boolean mScreenOn; |
| private boolean mLastScreenOn; |
| private boolean mIsFaceUnlockState; |
| private int mDensity; |
| private boolean mPulsing; |
| private boolean mDozing; |
| private boolean mBouncerVisible; |
| private boolean mLastDozing; |
| private boolean mLastPulsing; |
| private boolean mLastBouncerVisible; |
| private int mIconColor; |
| |
| private final Runnable mDrawOffTimeout = () -> update(true /* forceUpdate */); |
| private float mDozeAmount; |
| |
| private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = |
| new KeyguardUpdateMonitorCallback() { |
| @Override |
| public void onScreenTurnedOn() { |
| mScreenOn = true; |
| update(); |
| } |
| |
| @Override |
| public void onScreenTurnedOff() { |
| mScreenOn = false; |
| update(); |
| } |
| |
| @Override |
| public void onKeyguardVisibilityChanged(boolean showing) { |
| update(); |
| } |
| |
| @Override |
| public void onBiometricRunningStateChanged(boolean running, |
| BiometricSourceType biometricSourceType) { |
| update(); |
| } |
| |
| @Override |
| public void onStrongAuthStateChanged(int userId) { |
| update(); |
| } |
| }; |
| |
| @Inject |
| public LockIcon(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, |
| StatusBarStateController statusBarStateController, |
| ConfigurationController configurationController, |
| AccessibilityController accessibilityController) { |
| super(context, attrs); |
| mContext = context; |
| mUnlockMethodCache = UnlockMethodCache.getInstance(context); |
| mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); |
| mAccessibilityController = accessibilityController; |
| mConfigurationController = configurationController; |
| mStatusBarStateController = statusBarStateController; |
| onThemeChanged(); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| mStatusBarStateController.addCallback(this); |
| mConfigurationController.addCallback(this); |
| mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback); |
| mUnlockMethodCache.addListener(this); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mStatusBarStateController.removeCallback(this); |
| mConfigurationController.removeCallback(this); |
| mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback); |
| mUnlockMethodCache.removeListener(this); |
| } |
| |
| @Override |
| public void onThemeChanged() { |
| TypedArray typedArray = mContext.getTheme().obtainStyledAttributes( |
| null, new int[]{ R.attr.wallpaperTextColor }, 0, 0); |
| mIconColor = typedArray.getColor(0, Color.WHITE); |
| typedArray.recycle(); |
| updateDarkTint(); |
| } |
| |
| @Override |
| public void onUserInfoChanged(String name, Drawable picture, String userAccount) { |
| update(); |
| } |
| |
| /** |
| * If we're currently presenting an authentication error message. |
| */ |
| public void setTransientBiometricsError(boolean transientBiometricsError) { |
| mTransientBiometricsError = transientBiometricsError; |
| update(); |
| } |
| |
| @Override |
| protected void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| final int density = newConfig.densityDpi; |
| if (density != mDensity) { |
| mDensity = density; |
| update(); |
| } |
| } |
| |
| public void update() { |
| update(false /* force */); |
| } |
| |
| public void update(boolean force) { |
| int state = getState(); |
| mIsFaceUnlockState = state == STATE_SCANNING_FACE; |
| if (state != mLastState || mLastDozing != mDozing || mLastPulsing != mPulsing |
| || mLastScreenOn != mScreenOn || mLastBouncerVisible != mBouncerVisible || force) { |
| int iconAnimRes = getAnimationResForTransition(mLastState, state, mLastPulsing, |
| mPulsing, mLastDozing, mDozing, mBouncerVisible); |
| boolean isAnim = iconAnimRes != -1; |
| |
| Drawable icon; |
| if (isAnim) { |
| // Load the animation resource. |
| icon = mContext.getDrawable(iconAnimRes); |
| } else { |
| // Load the static icon resource based on the current state. |
| icon = getIconForState(state); |
| } |
| |
| final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable |
| ? (AnimatedVectorDrawable) icon |
| : null; |
| setImageDrawable(icon, false); |
| updateDarkTint(); |
| if (mIsFaceUnlockState) { |
| announceForAccessibility(getContext().getString( |
| R.string.accessibility_scanning_face)); |
| } |
| |
| if (animation != null && isAnim) { |
| animation.forceAnimationOnUI(); |
| animation.clearAnimationCallbacks(); |
| animation.registerAnimationCallback(new Animatable2.AnimationCallback() { |
| @Override |
| public void onAnimationEnd(Drawable drawable) { |
| if (getDrawable() == animation && state == getState() |
| && doesAnimationLoop(iconAnimRes)) { |
| animation.start(); |
| } |
| } |
| }); |
| animation.start(); |
| } |
| |
| if (isAnim && !mLastScreenOn) { |
| removeCallbacks(mDrawOffTimeout); |
| postDelayed(mDrawOffTimeout, FP_DRAW_OFF_TIMEOUT); |
| } else { |
| removeCallbacks(mDrawOffTimeout); |
| } |
| |
| mLastState = state; |
| mLastScreenOn = mScreenOn; |
| mLastDozing = mDozing; |
| mLastPulsing = mPulsing; |
| mLastBouncerVisible = mBouncerVisible; |
| } |
| |
| setVisibility(mDozing && !mPulsing ? INVISIBLE : VISIBLE); |
| updateClickability(); |
| } |
| |
| private void updateClickability() { |
| if (mAccessibilityController == null) { |
| return; |
| } |
| boolean canLock = mUnlockMethodCache.isMethodSecure() |
| && mUnlockMethodCache.canSkipBouncer(); |
| boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled(); |
| boolean clickToForceLock = canLock && !clickToUnlock; |
| boolean longClickToForceLock = canLock && !clickToForceLock; |
| setClickable(clickToForceLock || clickToUnlock); |
| setLongClickable(longClickToForceLock); |
| setFocusable(mAccessibilityController.isAccessibilityEnabled()); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| boolean fingerprintRunning = mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); |
| boolean unlockingAllowed = mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(); |
| if (fingerprintRunning && unlockingAllowed) { |
| AccessibilityNodeInfo.AccessibilityAction unlock |
| = new AccessibilityNodeInfo.AccessibilityAction( |
| AccessibilityNodeInfo.ACTION_CLICK, |
| getContext().getString(R.string.accessibility_unlock_without_fingerprint)); |
| info.addAction(unlock); |
| info.setHintText(getContext().getString( |
| R.string.accessibility_waiting_for_fingerprint)); |
| } else if (mIsFaceUnlockState) { |
| //Avoid 'button' to be spoken for scanning face |
| info.setClassName(LockIcon.class.getName()); |
| info.setContentDescription(getContext().getString( |
| R.string.accessibility_scanning_face)); |
| } |
| } |
| |
| private Drawable getIconForState(int state) { |
| int iconRes; |
| switch (state) { |
| case STATE_LOCKED: |
| // Scanning animation is a pulsing padlock. This means that the resting state is |
| // just a padlock. |
| case STATE_SCANNING_FACE: |
| // Error animation also starts and ands on the padlock. |
| case STATE_BIOMETRICS_ERROR: |
| iconRes = com.android.internal.R.drawable.ic_lock; |
| break; |
| case STATE_LOCK_OPEN: |
| iconRes = com.android.internal.R.drawable.ic_lock_open; |
| break; |
| default: |
| throw new IllegalArgumentException(); |
| } |
| |
| return mContext.getDrawable(iconRes); |
| } |
| |
| private boolean doesAnimationLoop(int resourceId) { |
| return resourceId == com.android.internal.R.anim.lock_scanning; |
| } |
| |
| private static int getAnimationResForTransition(int oldState, int newState, |
| boolean wasPulsing, boolean pulsing, boolean wasDozing, boolean dozing, |
| boolean bouncerVisible) { |
| |
| // Never animate when screen is off |
| if (dozing && !pulsing) { |
| return -1; |
| } |
| |
| boolean isError = oldState != STATE_BIOMETRICS_ERROR && newState == STATE_BIOMETRICS_ERROR; |
| boolean justUnlocked = oldState != STATE_LOCK_OPEN && newState == STATE_LOCK_OPEN; |
| boolean justLocked = oldState == STATE_LOCK_OPEN && newState == STATE_LOCKED; |
| |
| if (isError) { |
| return com.android.internal.R.anim.lock_to_error; |
| } else if (justUnlocked) { |
| return com.android.internal.R.anim.lock_unlock; |
| } else if (justLocked) { |
| return com.android.internal.R.anim.lock_lock; |
| } else if (newState == STATE_SCANNING_FACE && bouncerVisible) { |
| return com.android.internal.R.anim.lock_scanning; |
| } else if (!wasPulsing && pulsing && newState != STATE_LOCK_OPEN) { |
| return com.android.internal.R.anim.lock_in; |
| } |
| return -1; |
| } |
| |
| private int getState() { |
| KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); |
| if (mTransientBiometricsError) { |
| return STATE_BIOMETRICS_ERROR; |
| } else if (mUnlockMethodCache.canSkipBouncer()) { |
| return STATE_LOCK_OPEN; |
| } else if (updateMonitor.isFaceDetectionRunning()) { |
| return STATE_SCANNING_FACE; |
| } else { |
| return STATE_LOCKED; |
| } |
| } |
| |
| @Override |
| public void onDozeAmountChanged(float linear, float eased) { |
| mDozeAmount = eased; |
| updateDarkTint(); |
| } |
| |
| /** |
| * When keyguard is in pulsing (AOD2) state. |
| * @param pulsing {@code true} when pulsing. |
| */ |
| public void setPulsing(boolean pulsing) { |
| mPulsing = pulsing; |
| update(); |
| } |
| |
| /** |
| * Sets the dozing state of the keyguard. |
| */ |
| @Override |
| public void onDozingChanged(boolean dozing) { |
| mDozing = dozing; |
| update(); |
| } |
| |
| private void updateDarkTint() { |
| int color = ColorUtils.blendARGB(mIconColor, Color.WHITE, mDozeAmount); |
| setImageTintList(ColorStateList.valueOf(color)); |
| } |
| |
| /** |
| * If bouncer is visible or not. |
| */ |
| public void setBouncerVisible(boolean bouncerVisible) { |
| if (mBouncerVisible == bouncerVisible) { |
| return; |
| } |
| mBouncerVisible = bouncerVisible; |
| update(); |
| } |
| |
| @Override |
| public void onDensityOrFontScaleChanged() { |
| ViewGroup.LayoutParams lp = getLayoutParams(); |
| if (lp == null) { |
| return; |
| } |
| lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_width); |
| lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_height); |
| setLayoutParams(lp); |
| update(true /* force */); |
| } |
| |
| @Override |
| public void onLocaleListChanged() { |
| setContentDescription(getContext().getText(R.string.accessibility_unlock_button)); |
| update(true /* force */); |
| } |
| |
| @Override |
| public void onUnlockMethodStateChanged() { |
| update(); |
| } |
| } |