| /* |
| * Copyright (C) 2019 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.statusbar.phone.LockIcon.STATE_BIOMETRICS_ERROR; |
| import static com.android.systemui.statusbar.phone.LockIcon.STATE_LOCKED; |
| import static com.android.systemui.statusbar.phone.LockIcon.STATE_LOCK_OPEN; |
| import static com.android.systemui.statusbar.phone.LockIcon.STATE_SCANNING_FACE; |
| |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Color; |
| import android.hardware.biometrics.BiometricSourceType; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.logging.nano.MetricsProto; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.keyguard.KeyguardUpdateMonitor; |
| import com.android.keyguard.KeyguardUpdateMonitorCallback; |
| import com.android.systemui.R; |
| import com.android.systemui.dagger.qualifiers.Main; |
| import com.android.systemui.dock.DockManager; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.CommandQueue; |
| import com.android.systemui.statusbar.KeyguardIndicationController; |
| import com.android.systemui.statusbar.StatusBarState; |
| import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; |
| import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator.WakeUpListener; |
| import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent; |
| import com.android.systemui.statusbar.policy.AccessibilityController; |
| import com.android.systemui.statusbar.policy.ConfigurationController; |
| import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; |
| import com.android.systemui.statusbar.policy.KeyguardStateController; |
| |
| import java.util.Optional; |
| |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| |
| /** Controls the {@link LockIcon} in the lockscreen. */ |
| @Singleton |
| public class LockscreenLockIconController { |
| |
| private final LockscreenGestureLogger mLockscreenGestureLogger; |
| private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| private final LockPatternUtils mLockPatternUtils; |
| private final ShadeController mShadeController; |
| private final AccessibilityController mAccessibilityController; |
| private final KeyguardIndicationController mKeyguardIndicationController; |
| private final StatusBarStateController mStatusBarStateController; |
| private final ConfigurationController mConfigurationController; |
| private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator; |
| private final KeyguardBypassController mKeyguardBypassController; |
| private final Optional<DockManager> mDockManager; |
| private final KeyguardStateController mKeyguardStateController; |
| private final Resources mResources; |
| private final HeadsUpManagerPhone mHeadsUpManagerPhone; |
| private boolean mKeyguardShowing; |
| private boolean mKeyguardJustShown; |
| private boolean mBlockUpdates; |
| private boolean mSimLocked; |
| private boolean mTransientBiometricsError; |
| private boolean mDocked; |
| private boolean mWakeAndUnlockRunning; |
| private boolean mShowingLaunchAffordance; |
| private boolean mBouncerShowingScrimmed; |
| private boolean mFingerprintUnlock; |
| private int mStatusBarState = StatusBarState.SHADE; |
| private LockIcon mLockIcon; |
| |
| private View.OnAttachStateChangeListener mOnAttachStateChangeListener = |
| new View.OnAttachStateChangeListener() { |
| @Override |
| public void onViewAttachedToWindow(View v) { |
| mStatusBarStateController.addCallback(mSBStateListener); |
| mConfigurationController.addCallback(mConfigurationListener); |
| mNotificationWakeUpCoordinator.addListener(mWakeUpListener); |
| mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback); |
| mKeyguardStateController.addCallback(mKeyguardMonitorCallback); |
| |
| mDockManager.ifPresent(dockManager -> dockManager.addListener(mDockEventListener)); |
| |
| mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure(); |
| mConfigurationListener.onThemeChanged(); |
| update(); |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(View v) { |
| mStatusBarStateController.removeCallback(mSBStateListener); |
| mConfigurationController.removeCallback(mConfigurationListener); |
| mNotificationWakeUpCoordinator.removeListener(mWakeUpListener); |
| mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback); |
| mKeyguardStateController.removeCallback(mKeyguardMonitorCallback); |
| |
| mDockManager.ifPresent(dockManager -> dockManager.removeListener(mDockEventListener)); |
| } |
| }; |
| |
| private final StatusBarStateController.StateListener mSBStateListener = |
| new StatusBarStateController.StateListener() { |
| @Override |
| public void onDozingChanged(boolean isDozing) { |
| setDozing(isDozing); |
| } |
| |
| @Override |
| public void onPulsingChanged(boolean pulsing) { |
| setPulsing(pulsing); |
| } |
| |
| @Override |
| public void onDozeAmountChanged(float linear, float eased) { |
| if (mLockIcon != null) { |
| mLockIcon.setDozeAmount(eased); |
| } |
| } |
| |
| @Override |
| public void onStateChanged(int newState) { |
| setStatusBarState(newState); |
| } |
| }; |
| |
| private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { |
| private int mDensity; |
| |
| @Override |
| public void onThemeChanged() { |
| if (mLockIcon == null) { |
| return; |
| } |
| |
| TypedArray typedArray = mLockIcon.getContext().getTheme().obtainStyledAttributes( |
| null, new int[]{ R.attr.wallpaperTextColor }, 0, 0); |
| int iconColor = typedArray.getColor(0, Color.WHITE); |
| typedArray.recycle(); |
| mLockIcon.onThemeChange(iconColor); |
| } |
| |
| @Override |
| public void onDensityOrFontScaleChanged() { |
| if (mLockIcon == null) { |
| return; |
| } |
| |
| ViewGroup.LayoutParams lp = mLockIcon.getLayoutParams(); |
| if (lp == null) { |
| return; |
| } |
| lp.width = mLockIcon.getResources().getDimensionPixelSize(R.dimen.keyguard_lock_width); |
| lp.height = mLockIcon.getResources().getDimensionPixelSize( |
| R.dimen.keyguard_lock_height); |
| mLockIcon.setLayoutParams(lp); |
| update(true /* force */); |
| } |
| |
| @Override |
| public void onLocaleListChanged() { |
| if (mLockIcon == null) { |
| return; |
| } |
| |
| mLockIcon.setContentDescription( |
| mLockIcon.getResources().getText(R.string.accessibility_unlock_button)); |
| update(true /* force */); |
| } |
| |
| @Override |
| public void onConfigChanged(Configuration newConfig) { |
| final int density = newConfig.densityDpi; |
| if (density != mDensity) { |
| mDensity = density; |
| update(); |
| } |
| } |
| }; |
| |
| private final WakeUpListener mWakeUpListener = new WakeUpListener() { |
| @Override |
| public void onPulseExpansionChanged(boolean expandingChanged) { |
| } |
| |
| @Override |
| public void onFullyHiddenChanged(boolean isFullyHidden) { |
| if (mKeyguardBypassController.getBypassEnabled()) { |
| boolean changed = updateIconVisibility(); |
| if (changed) { |
| update(); |
| } |
| } |
| } |
| }; |
| |
| private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = |
| new KeyguardUpdateMonitorCallback() { |
| @Override |
| public void onSimStateChanged(int subId, int slotId, int simState) { |
| mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure(); |
| update(); |
| } |
| |
| @Override |
| public void onKeyguardVisibilityChanged(boolean showing) { |
| update(); |
| } |
| |
| @Override |
| public void onBiometricRunningStateChanged(boolean running, |
| BiometricSourceType biometricSourceType) { |
| update(); |
| } |
| |
| @Override |
| public void onStrongAuthStateChanged(int userId) { |
| update(); |
| } |
| }; |
| |
| private final DockManager.DockEventListener mDockEventListener = |
| event -> { |
| boolean docked = |
| event == DockManager.STATE_DOCKED || event == DockManager.STATE_DOCKED_HIDE; |
| if (docked != mDocked) { |
| mDocked = docked; |
| update(); |
| } |
| }; |
| |
| private final KeyguardStateController.Callback mKeyguardMonitorCallback = |
| new KeyguardStateController.Callback() { |
| @Override |
| public void onKeyguardShowingChanged() { |
| boolean force = false; |
| boolean wasShowing = mKeyguardShowing; |
| mKeyguardShowing = mKeyguardStateController.isShowing(); |
| if (!wasShowing && mKeyguardShowing && mBlockUpdates) { |
| mBlockUpdates = false; |
| force = true; |
| } |
| if (!wasShowing && mKeyguardShowing) { |
| mKeyguardJustShown = true; |
| } |
| update(force); |
| } |
| |
| @Override |
| public void onKeyguardFadingAwayChanged() { |
| if (!mKeyguardStateController.isKeyguardFadingAway()) { |
| if (mBlockUpdates) { |
| mBlockUpdates = false; |
| update(true /* force */); |
| } |
| } |
| } |
| |
| @Override |
| public void onUnlockedChanged() { |
| update(); |
| } |
| }; |
| |
| private final View.AccessibilityDelegate mAccessibilityDelegate = |
| new View.AccessibilityDelegate() { |
| @Override |
| public void onInitializeAccessibilityNodeInfo(View host, |
| AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(host, info); |
| boolean fingerprintRunning = |
| mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); |
| // Only checking if unlocking with Biometric is allowed (no matter strong or |
| // non-strong as long as primary auth, i.e. PIN/pattern/password, is not |
| // required), so it's ok to pass true for isStrongBiometric to |
| // isUnlockingWithBiometricAllowed() to bypass the check of whether non-strong |
| // biometric is allowed |
| boolean unlockingAllowed = mKeyguardUpdateMonitor |
| .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */); |
| if (fingerprintRunning && unlockingAllowed) { |
| AccessibilityNodeInfo.AccessibilityAction unlock = |
| new AccessibilityNodeInfo.AccessibilityAction( |
| AccessibilityNodeInfo.ACTION_CLICK, |
| mResources.getString( |
| R.string.accessibility_unlock_without_fingerprint)); |
| info.addAction(unlock); |
| info.setHintText(mResources.getString( |
| R.string.accessibility_waiting_for_fingerprint)); |
| } else if (getState() == STATE_SCANNING_FACE) { |
| //Avoid 'button' to be spoken for scanning face |
| info.setClassName(LockIcon.class.getName()); |
| info.setContentDescription(mResources.getString( |
| R.string.accessibility_scanning_face)); |
| } |
| } |
| }; |
| private int mLastState; |
| |
| @Inject |
| public LockscreenLockIconController(LockscreenGestureLogger lockscreenGestureLogger, |
| KeyguardUpdateMonitor keyguardUpdateMonitor, |
| LockPatternUtils lockPatternUtils, |
| ShadeController shadeController, |
| AccessibilityController accessibilityController, |
| KeyguardIndicationController keyguardIndicationController, |
| StatusBarStateController statusBarStateController, |
| ConfigurationController configurationController, |
| NotificationWakeUpCoordinator notificationWakeUpCoordinator, |
| KeyguardBypassController keyguardBypassController, |
| @Nullable DockManager dockManager, |
| KeyguardStateController keyguardStateController, |
| @Main Resources resources, |
| HeadsUpManagerPhone headsUpManagerPhone) { |
| mLockscreenGestureLogger = lockscreenGestureLogger; |
| mKeyguardUpdateMonitor = keyguardUpdateMonitor; |
| mLockPatternUtils = lockPatternUtils; |
| mShadeController = shadeController; |
| mAccessibilityController = accessibilityController; |
| mKeyguardIndicationController = keyguardIndicationController; |
| mStatusBarStateController = statusBarStateController; |
| mConfigurationController = configurationController; |
| mNotificationWakeUpCoordinator = notificationWakeUpCoordinator; |
| mKeyguardBypassController = keyguardBypassController; |
| mDockManager = dockManager == null ? Optional.empty() : Optional.of(dockManager); |
| mKeyguardStateController = keyguardStateController; |
| mResources = resources; |
| mHeadsUpManagerPhone = headsUpManagerPhone; |
| |
| mKeyguardIndicationController.setLockIconController(this); |
| } |
| |
| /** |
| * Associate the controller with a {@link LockIcon} |
| * |
| * TODO: change to an init method and inject the view. |
| */ |
| public void attach(LockIcon lockIcon) { |
| mLockIcon = lockIcon; |
| |
| mLockIcon.setOnClickListener(this::handleClick); |
| mLockIcon.setOnLongClickListener(this::handleLongClick); |
| mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate); |
| |
| if (mLockIcon.isAttachedToWindow()) { |
| mOnAttachStateChangeListener.onViewAttachedToWindow(mLockIcon); |
| } |
| mLockIcon.addOnAttachStateChangeListener(mOnAttachStateChangeListener); |
| setStatusBarState(mStatusBarStateController.getState()); |
| } |
| |
| public LockIcon getView() { |
| return mLockIcon; |
| } |
| |
| /** |
| * Called whenever the scrims become opaque, transparent or semi-transparent. |
| */ |
| public void onScrimVisibilityChanged(Integer scrimsVisible) { |
| if (mWakeAndUnlockRunning |
| && scrimsVisible == ScrimController.TRANSPARENT) { |
| mWakeAndUnlockRunning = false; |
| update(); |
| } |
| } |
| |
| /** |
| * Propagate {@link StatusBar} pulsing state. |
| */ |
| private void setPulsing(boolean pulsing) { |
| update(); |
| } |
| |
| /** |
| * We need to hide the lock whenever there's a fingerprint unlock, otherwise you'll see the |
| * icon on top of the black front scrim. |
| * We also want to halt padlock the animation when we're in face bypass mode or dismissing the |
| * keyguard with fingerprint. |
| * @param wakeAndUnlock are we wake and unlocking |
| * @param isUnlock are we currently unlocking |
| */ |
| public void onBiometricAuthModeChanged(boolean wakeAndUnlock, boolean isUnlock, |
| BiometricSourceType type) { |
| if (wakeAndUnlock) { |
| mWakeAndUnlockRunning = true; |
| } |
| mFingerprintUnlock = type == BiometricSourceType.FINGERPRINT; |
| if (isUnlock && (mFingerprintUnlock || mKeyguardBypassController.getBypassEnabled()) |
| && canBlockUpdates()) { |
| // We don't want the icon to change while we are unlocking |
| mBlockUpdates = true; |
| } |
| update(); |
| } |
| |
| /** |
| * When we're launching an affordance, like double pressing power to open camera. |
| */ |
| public void onShowingLaunchAffordanceChanged(Boolean showing) { |
| mShowingLaunchAffordance = showing; |
| update(); |
| } |
| |
| /** Sets whether the bouncer is showing. */ |
| public void setBouncerShowingScrimmed(boolean bouncerShowing) { |
| mBouncerShowingScrimmed = bouncerShowing; |
| if (mKeyguardBypassController.getBypassEnabled()) { |
| update(); |
| } |
| } |
| |
| /** |
| * Animate padlock opening when bouncer challenge is solved. |
| */ |
| public void onBouncerPreHideAnimation() { |
| update(); |
| } |
| |
| /** |
| * If we're currently presenting an authentication error message. |
| */ |
| public void setTransientBiometricsError(boolean transientBiometricsError) { |
| mTransientBiometricsError = transientBiometricsError; |
| update(); |
| } |
| |
| private boolean handleLongClick(View view) { |
| mLockscreenGestureLogger.write(MetricsProto.MetricsEvent.ACTION_LS_LOCK, |
| 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); |
| mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_LOCK_TAP); |
| mKeyguardIndicationController.showTransientIndication( |
| R.string.keyguard_indication_trust_disabled); |
| mKeyguardUpdateMonitor.onLockIconPressed(); |
| mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser()); |
| |
| return true; |
| } |
| |
| |
| private void handleClick(View view) { |
| if (!mAccessibilityController.isAccessibilityEnabled()) { |
| return; |
| } |
| mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */); |
| } |
| |
| private void update() { |
| update(false /* force */); |
| } |
| |
| private void update(boolean force) { |
| int state = getState(); |
| boolean shouldUpdate = mLastState != state || force; |
| if (mBlockUpdates && canBlockUpdates()) { |
| shouldUpdate = false; |
| } |
| if (shouldUpdate && mLockIcon != null) { |
| mLockIcon.update(state, mStatusBarStateController.isPulsing(), |
| mStatusBarStateController.isDozing(), mKeyguardJustShown); |
| } |
| mLastState = state; |
| mKeyguardJustShown = false; |
| updateIconVisibility(); |
| updateClickability(); |
| } |
| |
| private int getState() { |
| if ((mKeyguardStateController.canDismissLockScreen() |
| || !mKeyguardStateController.isShowing() |
| || mKeyguardStateController.isKeyguardGoingAway() |
| || mKeyguardStateController.isKeyguardFadingAway()) && !mSimLocked) { |
| return STATE_LOCK_OPEN; |
| } else if (mTransientBiometricsError) { |
| return STATE_BIOMETRICS_ERROR; |
| } else if (mKeyguardUpdateMonitor.isFaceDetectionRunning() |
| && !mStatusBarStateController.isPulsing()) { |
| return STATE_SCANNING_FACE; |
| } else { |
| return STATE_LOCKED; |
| } |
| } |
| |
| private boolean canBlockUpdates() { |
| return mKeyguardShowing || mKeyguardStateController.isKeyguardFadingAway(); |
| } |
| |
| private void setDozing(boolean isDozing) { |
| update(); |
| } |
| |
| /** Set the StatusBarState. */ |
| private void setStatusBarState(int statusBarState) { |
| mStatusBarState = statusBarState; |
| updateIconVisibility(); |
| } |
| |
| /** |
| * Update the icon visibility |
| * @return true if the visibility changed |
| */ |
| private boolean updateIconVisibility() { |
| boolean onAodNotPulsingOrDocked = mStatusBarStateController.isDozing() |
| && (!mStatusBarStateController.isPulsing() || mDocked); |
| boolean invisible = onAodNotPulsingOrDocked || mWakeAndUnlockRunning |
| || mShowingLaunchAffordance; |
| boolean fingerprintOrBypass = mFingerprintUnlock |
| || mKeyguardBypassController.getBypassEnabled(); |
| if (fingerprintOrBypass && !mBouncerShowingScrimmed) { |
| if ((mHeadsUpManagerPhone.isHeadsUpGoingAway() |
| || mHeadsUpManagerPhone.hasPinnedHeadsUp() |
| || mStatusBarState == StatusBarState.KEYGUARD |
| || mStatusBarState == StatusBarState.SHADE) |
| && !mNotificationWakeUpCoordinator.getNotificationsFullyHidden()) { |
| invisible = true; |
| } |
| } |
| |
| if (mLockIcon == null) { |
| return false; |
| } |
| |
| return mLockIcon.updateIconVisibility(!invisible); |
| } |
| |
| private void updateClickability() { |
| if (mAccessibilityController == null) { |
| return; |
| } |
| boolean canLock = mKeyguardStateController.isMethodSecure() |
| && mKeyguardStateController.canDismissLockScreen(); |
| boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled(); |
| if (mLockIcon != null) { |
| mLockIcon.setClickable(clickToUnlock); |
| mLockIcon.setLongClickable(canLock && !clickToUnlock); |
| mLockIcon.setFocusable(mAccessibilityController.isAccessibilityEnabled()); |
| } |
| } |
| |
| } |