/* * Copyright (C) 2020 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.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_PASSWORD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_SIM; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_PRIMARY; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE; import static com.android.keyguard.KeyguardSecurityModel.SecurityMode.SimPin; import static com.android.keyguard.KeyguardSecurityModel.SecurityMode.SimPuk; import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.admin.flags.Flags; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.media.AudioManager; import android.metrics.LogMaker; import android.os.SystemClock; import android.os.UserHandle; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.util.MathUtils; import android.util.Slog; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.window.OnBackAnimationCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityContainer.BouncerUiEvent; import com.android.keyguard.KeyguardSecurityContainer.SwipeListener; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; import dagger.Lazy; import kotlinx.coroutines.Job; import java.io.File; import java.util.Arrays; import javax.inject.Inject; import javax.inject.Provider; /** Controller for {@link KeyguardSecurityContainer} */ @KeyguardBouncerScope public class KeyguardSecurityContainerController extends ViewController implements KeyguardSecurityView { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardSecurityView"; private final AdminSecondaryLockScreenController mAdminSecondaryLockScreenController; private final LockPatternUtils mLockPatternUtils; private final KeyguardUpdateMonitor mUpdateMonitor; private final KeyguardSecurityModel mSecurityModel; private final MetricsLogger mMetricsLogger; private final UiEventLogger mUiEventLogger; private final KeyguardStateController mKeyguardStateController; private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController; private final ConfigurationController mConfigurationController; private final FalsingCollector mFalsingCollector; private final FalsingManager mFalsingManager; private final UserSwitcherController mUserSwitcherController; private final GlobalSettings mGlobalSettings; private final FeatureFlags mFeatureFlags; private final SessionTracker mSessionTracker; private final FalsingA11yDelegate mFalsingA11yDelegate; private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final DevicePolicyManager mDevicePolicyManager; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise // the audio service will bring up the volume dialog. private static final boolean KEYGUARD_MANAGES_VOLUME = false; private static final String ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key"; private final TelephonyManager mTelephonyManager; private final ViewMediatorCallback mViewMediatorCallback; private final AudioManager mAudioManager; private View.OnKeyListener mOnKeyListener = (v, keyCode, event) -> interceptMediaKey(event); private ActivityStarter.OnDismissAction mDismissAction; private Runnable mCancelAction; private boolean mWillRunDismissFromKeyguard; private int mLastOrientation; private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid; private int mCurrentUser = UserHandle.USER_NULL; private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = new UserSwitcherController.UserSwitchCallback() { @Override public void onUserSwitched() { if (mCurrentUser == mSelectedUserInteractor.getSelectedUserId()) { return; } mCurrentUser = mSelectedUserInteractor.getSelectedUserId(); showPrimarySecurityScreen(false); if (mCurrentSecurityMode != SimPin && mCurrentSecurityMode != SimPuk) { reinflateViewFlipper((l) -> { }); } } }; @VisibleForTesting final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() { private MotionEvent mTouchDown; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Do just a bit of our own falsing. People should only be tapping on the input, not // swiping. if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { // If we're in one handed mode, the user can tap on the opposite side of the screen // to move the bouncer across. In that case, inhibit the falsing (otherwise the taps // to move the bouncer to each screen side can end up closing it instead). if (mView.isTouchOnTheOtherSideOfSecurity(ev)) { mFalsingCollector.avoidGesture(); } if (mTouchDown != null) { mTouchDown.recycle(); mTouchDown = null; } mTouchDown = MotionEvent.obtain(ev); } else if (mTouchDown != null) { if (ev.getActionMasked() == MotionEvent.ACTION_UP || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { mTouchDown.recycle(); mTouchDown = null; } } return false; } }; private KeyguardSecurityCallback mKeyguardSecurityCallback = new KeyguardSecurityCallback() { @Override public void onUserInput() { mBouncerMessageInteractor.onPrimaryBouncerUserInput(); mDeviceEntryFaceAuthInteractor.onPrimaryBouncerUserInput(); } @Override public void dismiss(boolean authenticated, int targetId, SecurityMode expectedSecurityMode) { dismiss(authenticated, targetId, /* bypassSecondaryLockScreen */ false, expectedSecurityMode); } @Override public boolean dismiss(boolean authenticated, int targetId, boolean bypassSecondaryLockScreen, SecurityMode expectedSecurityMode) { return showNextSecurityScreenOrFinish( authenticated, targetId, bypassSecondaryLockScreen, expectedSecurityMode); } @Override public void userActivity() { mViewMediatorCallback.userActivity(); } @Override public boolean isVerifyUnlockOnly() { return false; } @Override public void onAttemptLockoutStart(long seconds) { mBouncerMessageInteractor.onPrimaryAuthLockedOut(seconds); } @Override public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { if (timeoutMs == 0 && !success) { mBouncerMessageInteractor.onPrimaryAuthIncorrectAttempt(); } int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT; if (mView.isSidedSecurityMode()) { bouncerSide = mView.isSecurityLeftAligned() ? SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__LEFT : SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__RIGHT; } if (success) { SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED, SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__SUCCESS, bouncerSide); mLockPatternUtils.reportSuccessfulPasswordAttempt(userId); // Force a garbage collection in an attempt to erase any lockscreen password left in // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard // dismiss animation janky. ThreadUtils.postOnBackgroundThread(() -> { try { Thread.sleep(5000); } catch (InterruptedException ignored) { } System.gc(); System.runFinalization(); System.gc(); }); } else { SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED, SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__FAILURE, bouncerSide); reportFailedUnlockAttempt(userId, timeoutMs); } mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER) .setType(success ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_FAILURE)); mUiEventLogger.log(success ? BouncerUiEvent.BOUNCER_PASSWORD_SUCCESS : BouncerUiEvent.BOUNCER_PASSWORD_FAILURE, getSessionId()); } @Override public void reset() { mViewMediatorCallback.resetKeyguard(); } @Override public void onCancelClicked() { mViewMediatorCallback.onCancelClicked(); } /** * Authentication has happened and it's time to dismiss keyguard. This function * should clean up and inform KeyguardViewMediator. * * @param targetUserId a user that needs to be the foreground user at the dismissal * completion. */ @Override public void finish(int targetUserId) { if (!SceneContainerFlag.isEnabled()) { // If there's a pending runnable because the user interacted with a widget // and we're leaving keyguard, then run it. boolean deferKeyguardDone = false; mWillRunDismissFromKeyguard = false; if (mDismissAction != null) { deferKeyguardDone = mDismissAction.onDismiss(); mWillRunDismissFromKeyguard = mDismissAction.willRunAnimationOnKeyguard(); mDismissAction = null; mCancelAction = null; } if (mViewMediatorCallback != null) { if (deferKeyguardDone) { mViewMediatorCallback.keyguardDonePending(targetUserId); } else { mViewMediatorCallback.keyguardDone(targetUserId); } } } if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardTransitionInteractor.startDismissKeyguardTransition( "KeyguardSecurityContainerController#finish"); } } @Override public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput) { mViewMediatorCallback.setNeedsInput(needsInput); } @Override public void showCurrentSecurityScreen() { showPrimarySecurityScreen(false); } }; private final SwipeListener mSwipeListener = new SwipeListener() { @Override public void onSwipeUp() { if (mDeviceEntryFaceAuthInteractor.canFaceAuthRun()) { mKeyguardSecurityCallback.userActivity(); } mDeviceEntryFaceAuthInteractor.onSwipeUpOnBouncer(); if (mDeviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()) { mUpdateMonitor.requestActiveUnlock( ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT_LEGACY, "swipeUpOnBouncer"); } } @Override public void onSwipeDown() { mViewMediatorCallback.onBouncerSwipeDown(); } }; private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @Override public void onThemeChanged() { reloadColors(); } @Override public void onUiModeChanged() { reloadColors(); } @Override public void onDensityOrFontScaleChanged() { KeyguardSecurityContainerController.this .onDensityOrFontScaleOrOrientationChanged(); } @Override public void onOrientationChanged(int orientation) { if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE) && getResources().getBoolean(R.bool.update_bouncer_constraints)) { boolean useSplitBouncer = orientation == ORIENTATION_LANDSCAPE; mSecurityViewFlipperController.updateConstraints(useSplitBouncer); } } @Override public void onConfigChanged(Configuration newConfig) { configureMode(); } }; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @Override public void onTrustGrantedForCurrentUser( boolean dismissKeyguard, boolean newlyUnlocked, TrustGrantFlags flags, String message ) { if (dismissKeyguard) { if (!mView.isVisibleToUser()) { // The trust agent dismissed the keyguard without the user proving // that they are present (by swiping up to show the bouncer). That's // fine if the user proved presence via some other way to the trust // agent. Log.i(TAG, "TrustAgent dismissed Keyguard."); } mKeyguardSecurityCallback.dismiss( false /* authenticated */, mSelectedUserInteractor.getSelectedUserId(), /* bypassSecondaryLockScreen */ false, SecurityMode.Invalid ); } else { if (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) { mViewMediatorCallback.playTrustedSound(); } } } @Override public void onDevicePolicyManagerStateChanged() { showPrimarySecurityScreen(false); } }; private final SelectedUserInteractor mSelectedUserInteractor; private final Provider mDeviceEntryInteractor; private final Provider mJavaAdapter; private final DeviceProvisionedController mDeviceProvisionedController; private final Lazy mPrimaryBouncerInteractor; @Nullable private Job mSceneTransitionCollectionJob; @Inject public KeyguardSecurityContainerController(KeyguardSecurityContainer view, AdminSecondaryLockScreenController.Factory adminSecondaryLockScreenControllerFactory, LockPatternUtils lockPatternUtils, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardSecurityModel keyguardSecurityModel, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, KeyguardStateController keyguardStateController, KeyguardSecurityViewFlipperController securityViewFlipperController, ConfigurationController configurationController, FalsingCollector falsingCollector, FalsingManager falsingManager, UserSwitcherController userSwitcherController, FeatureFlags featureFlags, GlobalSettings globalSettings, SessionTracker sessionTracker, FalsingA11yDelegate falsingA11yDelegate, TelephonyManager telephonyManager, ViewMediatorCallback viewMediatorCallback, AudioManager audioManager, DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, BouncerMessageInteractor bouncerMessageInteractor, Provider javaAdapter, SelectedUserInteractor selectedUserInteractor, DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, DevicePolicyManager devicePolicyManager, KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy primaryBouncerInteractor, Provider deviceEntryInteractor ) { super(view); view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; mSecurityModel = keyguardSecurityModel; mMetricsLogger = metricsLogger; mUiEventLogger = uiEventLogger; mKeyguardStateController = keyguardStateController; mSecurityViewFlipperController = securityViewFlipperController; mAdminSecondaryLockScreenController = adminSecondaryLockScreenControllerFactory.create( mKeyguardSecurityCallback); mConfigurationController = configurationController; mLastOrientation = getResources().getConfiguration().orientation; mFalsingCollector = falsingCollector; mFalsingManager = falsingManager; mUserSwitcherController = userSwitcherController; mFeatureFlags = featureFlags; mGlobalSettings = globalSettings; mSessionTracker = sessionTracker; mFalsingA11yDelegate = falsingA11yDelegate; mTelephonyManager = telephonyManager; mViewMediatorCallback = viewMediatorCallback; mAudioManager = audioManager; mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; mBouncerMessageInteractor = bouncerMessageInteractor; mSelectedUserInteractor = selectedUserInteractor; mDeviceEntryInteractor = deviceEntryInteractor; mJavaAdapter = javaAdapter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mDeviceProvisionedController = deviceProvisionedController; mPrimaryBouncerInteractor = primaryBouncerInteractor; mDevicePolicyManager = devicePolicyManager; } @Override public void onInit() { mSecurityViewFlipperController.init(); updateResources(); configureMode(); } @Override protected void onViewAttached() { mUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mView.setSwipeListener(mSwipeListener); mView.addMotionEventListener(mGlobalTouchListener); mConfigurationController.addCallback(mConfigurationListener); mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback); mView.setViewMediatorCallback(mViewMediatorCallback); // Update ViewMediator with the current input method requirements mViewMediatorCallback.setNeedsInput(needsInput()); mView.setOnKeyListener(mOnKeyListener); showPrimarySecurityScreen(false); if (SceneContainerFlag.isEnabled()) { // When the scene framework says that the lockscreen has been dismissed, dismiss the // keyguard here, revealing the underlying app or launcher: mSceneTransitionCollectionJob = mJavaAdapter.get().alwaysCollectFlow( mDeviceEntryInteractor.get().isDeviceEntered(), isDeviceEntered -> { if (isDeviceEntered) { final int selectedUserId = mSelectedUserInteractor.getSelectedUserId(); showNextSecurityScreenOrFinish( /* authenticated= */ true, selectedUserId, /* bypassSecondaryLockScreen= */ true, mSecurityModel.getSecurityMode(selectedUserId)); } } ); } } @Override protected void onViewDetached() { mUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); mConfigurationController.removeCallback(mConfigurationListener); mView.removeMotionEventListener(mGlobalTouchListener); mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback); if (mSceneTransitionCollectionJob != null) { mSceneTransitionCollectionJob.cancel(null); mSceneTransitionCollectionJob = null; } } /** */ public void onPause() { if (DEBUG) { Log.d(TAG, String.format("screen off, instance %s at %s", Integer.toHexString(hashCode()), SystemClock.uptimeMillis())); } showPrimarySecurityScreen(true); mAdminSecondaryLockScreenController.hide(); if (mCurrentSecurityMode != SecurityMode.None) { getCurrentSecurityController(controller -> controller.onPause()); } mView.onPause(); mView.clearFocus(); } /** * Shows the primary security screen for the user. This will be either the multi-selector * or the user's security method. * * @param turningOff true if the device is being turned off */ public void showPrimarySecurityScreen(boolean turningOff) { if (DEBUG) Log.d(TAG, "show()"); SecurityMode securityMode = whitelistIpcs(() -> mSecurityModel.getSecurityMode( mSelectedUserInteractor.getSelectedUserId())); if (DEBUG) Log.v(TAG, "showPrimarySecurityScreen(turningOff=" + turningOff + ")"); mPrimaryBouncerInteractor.get().setLastShownPrimarySecurityScreen(securityMode); showSecurityScreen(securityMode); } /** * Show a string explaining why the security view needs to be solved. * * @param reason a flag indicating which string should be shown, see * {@link KeyguardSecurityView#PROMPT_REASON_NONE}, * {@link KeyguardSecurityView#PROMPT_REASON_RESTART}, * {@link KeyguardSecurityView#PROMPT_REASON_TIMEOUT}, and * {@link KeyguardSecurityView#PROMPT_REASON_PREPARE_FOR_UPDATE}. */ @Override public void showPromptReason(int reason) { if (mCurrentSecurityMode != SecurityMode.None) { if (reason != PROMPT_REASON_NONE) { Log.i(TAG, "Strong auth required, reason: " + reason); } getCurrentSecurityController(controller -> controller.showPromptReason(reason)); } } /** Set message of bouncer title. */ public void showMessage(CharSequence message, ColorStateList colorState, boolean animated) { if (mCurrentSecurityMode != SecurityMode.None) { getCurrentSecurityController( controller -> controller.showMessage(message, colorState, animated)); } } /** * Sets an action to run when keyguard finishes. * * @param action callback to be invoked when keyguard disappear animation completes. */ public void setOnDismissAction(ActivityStarter.OnDismissAction action, Runnable cancelAction) { if (SceneContainerFlag.isEnabled()) { return; } if (mCancelAction != null) { mCancelAction.run(); } mDismissAction = action; mCancelAction = cancelAction; } /** * @return whether dismiss action or cancel action has been set. */ public boolean hasDismissActions() { return mDismissAction != null || mCancelAction != null; } /** * @return will the dismissal run from the keyguard layout (instead of from bouncer) */ public boolean willRunDismissFromKeyguard() { return mWillRunDismissFromKeyguard; } /** * Remove any dismiss action or cancel action that was set. */ public void cancelDismissAction() { setOnDismissAction(null, null); } /** * Potentially dismiss the current security screen, after validating that all device * security has been unlocked. Otherwise show the next screen. */ public void dismiss(boolean authenticated, int targetUserId, SecurityMode expectedSecurityMode) { mKeyguardSecurityCallback.dismiss(authenticated, targetUserId, expectedSecurityMode); } /** * Dismisses the keyguard by going to the next screen or making it gone. * * @param targetUserId a user that needs to be the foreground user at the dismissal completion. * @return True if the keyguard is done. */ public boolean dismiss(int targetUserId) { return mKeyguardSecurityCallback.dismiss(false, targetUserId, false, getCurrentSecurityMode()); } public SecurityMode getCurrentSecurityMode() { return mCurrentSecurityMode; } /** * @return the top of the corresponding view. */ public int getTop() { int top = mView.getTop(); // The password view has an extra top padding that should be ignored. if (getCurrentSecurityMode() == SecurityMode.Password) { View messageArea = mView.findViewById(R.id.keyguard_message_area); top += messageArea.getTop(); } return top; } /** Set true if the view can be interacted with */ public void setInteractable(boolean isInteractable) { mView.setInteractable(isInteractable); } /** * Dismiss keyguard due to a user unlock event. */ public void finish(int currentUser) { mKeyguardSecurityCallback.finish(currentUser); } /** * @return the text of the KeyguardMessageArea. */ public CharSequence getTitle() { return mView.getTitle(); } /** * Resets the state of the views. */ public void reset() { mView.reset(); mSecurityViewFlipperController.reset(); } /** Prepares views in the bouncer before starting appear animation. */ public void prepareToShow() { View bouncerUserSwitcher = mView.findViewById(R.id.keyguard_bouncer_user_switcher); if (bouncerUserSwitcher != null) { bouncerUserSwitcher.setAlpha(0f); } } @Override public void onResume(int reason) { if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode())); mView.requestFocus(); if (mCurrentSecurityMode != SecurityMode.None) { int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN; if (mView.isSidedSecurityMode()) { state = mView.isSecurityLeftAligned() ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_LEFT : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_RIGHT; } SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, state); getCurrentSecurityController(controller -> controller.onResume(reason)); } mView.onResume( mSecurityModel.getSecurityMode(mSelectedUserInteractor.getSelectedUserId()), mKeyguardStateController.isFaceEnrolledAndEnabled()); } /** Sets an initial message that would override the default message */ public void setInitialMessage() { CharSequence customMessage = mViewMediatorCallback.consumeCustomMessage(); if (!TextUtils.isEmpty(customMessage)) { showMessage(customMessage, /* colorState= */ null, /* animated= */ false); return; } showPromptReason(mViewMediatorCallback.getBouncerPromptReason()); } /** * Show the bouncer and start appear animations. */ public void appear() { // We might still be collapsed and the view didn't have time to layout yet or still // be small, let's wait on the predraw to do the animation in that case. mView.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { mView.getViewTreeObserver().removeOnPreDrawListener(this); startAppearAnimation(); return true; } }); mView.requestLayout(); } public void startAppearAnimation() { if (mCurrentSecurityMode != SecurityMode.None) { mView.startAppearAnimation(mCurrentSecurityMode); getCurrentSecurityController(controller -> controller.startAppearAnimation()); } } /** Set the alpha of the security container view */ public void setAlpha(float alpha) { mView.setAlpha(alpha); } public boolean startDisappearAnimation(Runnable onFinishRunnable) { if (mCurrentSecurityMode != SecurityMode.None) { mView.startDisappearAnimation(mCurrentSecurityMode); getCurrentSecurityController( controller -> { boolean didRunAnimation = controller.startDisappearAnimation( onFinishRunnable); if (!didRunAnimation && onFinishRunnable != null) { onFinishRunnable.run(); } }); } return true; } public void onStartingToHide() { if (mCurrentSecurityMode != SecurityMode.None) { getCurrentSecurityController(controller -> controller.onStartingToHide()); } } /** Called when the bouncer changes visibility. */ public void onBouncerVisibilityChanged(boolean isVisible) { if (!isVisible) { mView.resetScale(); } } /** * Shows the next security screen if there is one. * * @param authenticated true if the user entered the correct authentication * @param targetUserId a user that needs to be the foreground user at the finish * (if called) * completion. * @param bypassSecondaryLockScreen true if the user is allowed to bypass the secondary * secondary lock screen requirement, if any. * @param expectedSecurityMode SecurityMode that is invoking this request. * SecurityMode.Invalid * indicates that no check should be done * @return true if keyguard is done */ public boolean showNextSecurityScreenOrFinish(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen, SecurityMode expectedSecurityMode) { if (DEBUG) Log.d(TAG, "showNextSecurityScreenOrFinish(" + authenticated + ")"); if (expectedSecurityMode != SecurityMode.Invalid && expectedSecurityMode != getCurrentSecurityMode()) { Log.w(TAG, "Attempted to invoke showNextSecurityScreenOrFinish with securityMode " + expectedSecurityMode + ", but current mode is " + getCurrentSecurityMode()); return false; } boolean authenticatedWithPrimaryAuth = false; boolean finish = false; int eventSubtype = -1; BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN; if (mUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()) { finish = true; eventSubtype = BOUNCER_DISMISSIBLE_KEYGUARD; // TODO: b/308417021 add UI event } else if (mUpdateMonitor.getUserHasTrust(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_EXTENDED_ACCESS; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS; } else if (mUpdateMonitor.getUserUnlockedWithBiometric(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_BIOMETRIC; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_BIOMETRIC; } else if (SecurityMode.None == getCurrentSecurityMode()) { SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); if (SecurityMode.None == securityMode) { finish = true; // no security required eventSubtype = BOUNCER_DISMISS_NONE_SECURITY; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_NONE_SECURITY; } else { showSecurityScreen(securityMode); // switch to the alternate security view } } else if (authenticated) { switch (getCurrentSecurityMode()) { case Pattern: case Password: case PIN: authenticatedWithPrimaryAuth = true; finish = true; eventSubtype = BOUNCER_DISMISS_PASSWORD; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_PASSWORD; break; case SimPin: case SimPuk: // Shortcut for SIM PIN/PUK to go to directly to user's security screen or home SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); boolean isLockscreenDisabled = mLockPatternUtils.isLockScreenDisabled( mSelectedUserInteractor.getSelectedUserId()) || !mDeviceProvisionedController.isUserSetup(targetUserId); if (securityMode == SecurityMode.None && isLockscreenDisabled) { finish = true; eventSubtype = BOUNCER_DISMISS_SIM; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM; } else if (Arrays.asList(SimPin, SimPuk).contains(securityMode)) { // There are additional screens to the sim pin/puk flow. showSecurityScreen(securityMode); } break; default: Log.v(TAG, "Bad security screen " + getCurrentSecurityMode() + ", fail safe"); showPrimarySecurityScreen(false); break; } } // Check for device admin specified additional security measures. if (finish && !bypassSecondaryLockScreen) { Intent secondaryLockscreenIntent = mUpdateMonitor.getSecondaryLockscreenRequirement(targetUserId); if (secondaryLockscreenIntent != null) { mAdminSecondaryLockScreenController.show(secondaryLockscreenIntent); return false; } } if (eventSubtype != -1) { mMetricsLogger.write(new LogMaker(MetricsProto.MetricsEvent.BOUNCER) .setType(MetricsProto.MetricsEvent.TYPE_DISMISS).setSubtype(eventSubtype)); } if (uiEvent != BouncerUiEvent.UNKNOWN) { mUiEventLogger.log(uiEvent, getSessionId()); } if (SceneContainerFlag.isEnabled()) { if (authenticatedWithPrimaryAuth) { mPrimaryBouncerInteractor.get() .notifyKeyguardAuthenticatedPrimaryAuth(targetUserId); } else if (finish) { mPrimaryBouncerInteractor.get().notifyUserRequestedBouncerWhenAlreadyAuthenticated( targetUserId); } } if (finish) { mKeyguardSecurityCallback.finish(targetUserId); } return finish; } @Override public boolean needsInput() { return false; } /** * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture. */ @NonNull public OnBackAnimationCallback getBackCallback() { return mView.getBackCallback(); } /** * @return whether we should dispatch the back key event before Ime. */ public boolean dispatchBackKeyEventPreIme() { return getCurrentSecurityMode() == SecurityMode.Password; } /** * Allows the media keys to work when the keyguard is showing. * The media keys should be of no interest to the actual keyguard view(s), * so intercepting them here should not be of any harm. * * @param event The key event * @return whether the event was consumed as a media key. */ public boolean interceptMediaKey(KeyEvent event) { int keyCode = event.getKeyCode(); if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: /* Suppress PLAY/PAUSE toggle when phone is ringing or * in-call to avoid music playback */ if (mTelephonyManager != null && mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) { return true; // suppress key event } return false; case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: case KeyEvent.KEYCODE_MEDIA_RECORD: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { handleMediaKeyEvent(event); return true; } case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { if (KEYGUARD_MANAGES_VOLUME) { // Volume buttons should only function for music (local or remote). // TODO: Actually handle MUTE. mAudioManager.adjustSuggestedStreamVolume( keyCode == KeyEvent.KEYCODE_VOLUME_UP ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER /* direction */, AudioManager.STREAM_MUSIC /* stream */, 0 /* flags */); // Don't execute default volume behavior return true; } else { return false; } } } } else if (event.getAction() == KeyEvent.ACTION_UP) { switch (keyCode) { case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: case KeyEvent.KEYCODE_MEDIA_RECORD: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { handleMediaKeyEvent(event); return true; } } } return false; } private void handleMediaKeyEvent(KeyEvent keyEvent) { mAudioManager.dispatchMediaKeyEvent(keyEvent); } /** * In general, we enable unlocking the insecure keyguard with the menu key. However, there are * some cases where we wish to disable it, notably when the menu button placement or technology * is prone to false positives. * * @return true if the menu key should be enabled */ public boolean shouldEnableMenuKey() { final Resources res = mView.getResources(); final boolean configDisabled = res.getBoolean(R.bool.config_disableMenuKeyInLockScreen); final boolean isTestHarness = ActivityManager.isRunningInTestHarness(); final boolean fileOverride = (new File(ENABLE_MENU_KEY_FILE)).exists(); return !configDisabled || isTestHarness || fileOverride; } /** * Switches to the given security view unless it's already being shown, in which case * this is a no-op. */ @VisibleForTesting void showSecurityScreen(SecurityMode securityMode) { if (DEBUG) Log.d(TAG, "showSecurityScreen(" + securityMode + ")"); if (securityMode == SecurityMode.Invalid || securityMode == mCurrentSecurityMode) { return; } getCurrentSecurityController(oldView -> oldView.onPause()); mCurrentSecurityMode = securityMode; getCurrentSecurityController( newView -> { newView.onResume(KeyguardSecurityView.VIEW_REVEALED); mSecurityViewFlipperController.show(newView); configureMode(); mKeyguardSecurityCallback.onSecurityModeChanged( securityMode, newView != null && newView.needsInput()); }); } /** * Returns whether the given security view should be used in a "one handed" way. This can be * used to change how the security view is drawn (e.g. take up less of the screen, and align to * one side). */ private boolean canUseOneHandedBouncer() { return switch (mCurrentSecurityMode) { case PIN, Pattern, SimPin, SimPuk -> getResources().getBoolean( R.bool.can_use_one_handed_bouncer); default -> false; }; } private boolean canDisplayUserSwitcher() { return getContext().getResources().getBoolean(R.bool.config_enableBouncerUserSwitcher); } private void configureMode() { boolean useSimSecurity = mCurrentSecurityMode == SimPin || mCurrentSecurityMode == SimPuk; int mode = KeyguardSecurityContainer.MODE_DEFAULT; if (canDisplayUserSwitcher() && !useSimSecurity) { mode = KeyguardSecurityContainer.MODE_USER_SWITCHER; } else if (canUseOneHandedBouncer()) { mode = KeyguardSecurityContainer.MODE_ONE_HANDED; } mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue), /* colorState= */ null, /* animated= */ true), mFalsingA11yDelegate); } public void reportFailedUnlockAttempt(int userId, int timeoutMs) { // +1 for this time final int failedAttempts = mLockPatternUtils.getCurrentFailedPasswordAttempts(userId) + 1; if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); final int failedAttemptsBeforeWipe = mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId); final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? (failedAttemptsBeforeWipe - failedAttempts) : Integer.MAX_VALUE; // because DPM returns 0 if no restriction if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { // The user has installed a DevicePolicyManager that requests a // user/profile to be wiped N attempts. Once we get below the grace period, // we post this dialog every time as a clear warning until the deletion // fires. Check which profile has the strictest policy for failed password // attempts. final int expiringUser = mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId); Integer mainUser = mSelectedUserInteractor.getMainUserId(); showMessageForFailedUnlockAttempt( userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts); } mLockPatternUtils.reportFailedPasswordAttempt(userId); if (timeoutMs > 0) { mLockPatternUtils.reportPasswordLockout(timeoutMs, userId); if (!com.android.systemui.Flags.revampedBouncerMessages()) { mView.showTimeoutDialog(userId, timeoutMs, mLockPatternUtils, mSecurityModel.getSecurityMode(userId)); } } } @VisibleForTesting void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId, int remainingBeforeWipe, int failedAttempts) { int userType = USER_TYPE_PRIMARY; if (expiringUserId == userId) { int primaryUser = UserHandle.USER_SYSTEM; if (Flags.headlessSingleUserFixes()) { if (mainUserId != null) { primaryUser = mainUserId; } } // TODO: http://b/23522538 if (expiringUserId != primaryUser) { userType = USER_TYPE_SECONDARY_USER; } } else if (expiringUserId != UserHandle.USER_NULL) { userType = USER_TYPE_WORK_PROFILE; } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY if (remainingBeforeWipe > 0) { mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); } else { // Too many attempts. The device will be wiped shortly. Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId + " will be wiped!"); mView.showWipeDialog(failedAttempts, userType); } } private void getCurrentSecurityController( KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) { mSecurityViewFlipperController .getSecurityView(mCurrentSecurityMode, mKeyguardSecurityCallback, onViewInflatedCallback); } /** * Apply keyguard configuration from the currently active resources. This can be called when the * device configuration changes, to re-apply some resources that are qualified on the device * configuration. */ public void updateResources() { int gravity; Resources resources = mView.getResources(); if (resources.getBoolean(R.bool.can_use_one_handed_bouncer)) { gravity = resources.getInteger( R.integer.keyguard_host_view_one_handed_gravity); } else { gravity = resources.getInteger(R.integer.keyguard_host_view_gravity); } mTranslationY = resources .getDimensionPixelSize(R.dimen.keyguard_host_view_translation_y); // Android SysUI uses a FrameLayout as the top-level, but Auto uses RelativeLayout. // We're just changing the gravity here though (which can't be applied to RelativeLayout), // so only attempt the update if mView is inside a FrameLayout. if (mView.getLayoutParams() instanceof FrameLayout.LayoutParams) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mView.getLayoutParams(); if (lp.gravity != gravity) { lp.gravity = gravity; mView.setLayoutParams(lp); } } int newOrientation = getResources().getConfiguration().orientation; if (newOrientation != mLastOrientation) { mLastOrientation = newOrientation; configureMode(); } } private @Nullable InstanceId getSessionId() { return mSessionTracker.getSessionId(SESSION_KEYGUARD); } /** Update keyguard position based on a tapped X coordinate. */ public void updateKeyguardPosition(float x) { mView.updatePositionByTouchX(x); } private void reloadColors() { mView.reloadColors(); } /** Handles density or font scale changes. */ private void onDensityOrFontScaleOrOrientationChanged() { reinflateViewFlipper(controller -> mView.onDensityOrFontScaleChanged()); } /** * Reinflate the view flipper child view. */ public void reinflateViewFlipper( KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedListener) { mSecurityViewFlipperController.clearViews(); mSecurityViewFlipperController.asynchronouslyInflateView(mCurrentSecurityMode, mKeyguardSecurityCallback, onViewInflatedListener); } /** * Fades and translates in/out the security screen. * Fades in as expansion approaches 0. * Animation duration is between 0.33f and 0.67f of panel expansion fraction. * * @param fraction amount of the screen that should show. */ public void setExpansion(float fraction) { float scaledFraction = BouncerPanelExpansionCalculator.showBouncerProgress(fraction); setAlpha(MathUtils.constrain(1 - scaledFraction, 0f, 1f)); mView.setTranslationY(scaledFraction * mTranslationY); } }