Merge changes I04f6a54e,Ifb505f6d,Ia9ec3ce2,I97b201c6,Ia12e8cd6, ... into udc-dev

* changes:
  Add an audit logger for bouncer messages
  Add BouncerMessageRepository and BouncerMessageInteractor
  Make current user strong auth flags available in BiometricSettingsRepository
  Hide existing bouncer message area if the feature flag is enabled.
  Create BouncerMessageFactory to easily create BouncerMessage data model.
  Add views for displaying the new bouncer messages
This commit is contained in:
Chandru S
2023-05-13 06:45:03 +00:00
committed by Android (Google) Code Review
52 changed files with 2262 additions and 153 deletions

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2023 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
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<com.android.keyguard.BouncerKeyguardMessageArea
android:id="@+id/bouncer_primary_message_area"
style="@style/Keyguard.Bouncer.PrimaryMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyguard_lock_padding"
android:focusable="true"
/>
<com.android.keyguard.BouncerKeyguardMessageArea
android:id="@+id/bouncer_secondary_message_area"
style="@style/Keyguard.Bouncer.SecondaryMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/secondary_message_padding"
android:focusable="true" />
</merge>

View File

@@ -29,6 +29,13 @@
>
<include layout="@layout/keyguard_bouncer_message_area"/>
<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
/>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"

View File

@@ -33,6 +33,12 @@
android:clipToPadding="false">
<include layout="@layout/keyguard_bouncer_message_area"/>
<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pattern_container"
android:layout_width="match_parent"

View File

@@ -29,6 +29,12 @@
androidprv:layout_maxWidth="@dimen/keyguard_security_width">
<include layout="@layout/keyguard_bouncer_message_area"/>
<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pin_container"
android:layout_width="match_parent"

View File

@@ -23,6 +23,26 @@
<style name="Keyguard.TextView" parent="@android:style/Widget.DeviceDefault.TextView">
<item name="android:textSize">@dimen/kg_status_line_font_size</item>
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="Keyguard.Bouncer.PrimaryMessage" parent="Theme.SystemUI">
<item name="android:textSize">18sp</item>
<item name="android:lineHeight">24dp</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
<item name="android:singleLine">true</item>
<item name="android:textAlignment">center</item>
<item name="android:ellipsize">marquee</item>
</style>
<style name="Keyguard.Bouncer.SecondaryMessage" parent="Theme.SystemUI">
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20dp</item>
<item name="android:maxLines">@integer/bouncer_secondary_message_lines</item>
<item name="android:lines">@integer/bouncer_secondary_message_lines</item>
<item name="android:textAlignment">center</item>
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
<item name="android:ellipsize">end</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
</style>
<style name="Keyguard.TextView.EmergencyButton" parent="Theme.SystemUI">
<item name="android:textColor">?androidprv:attr/materialColorOnTertiaryFixed</item>

View File

@@ -577,6 +577,9 @@
<!-- Whether to show the side fps hint while on bouncer -->
<bool name="config_show_sidefps_hint_on_bouncer">true</bool>
<!-- Max number of lines we want to show for the bouncer secondary message -->
<integer name="bouncer_secondary_message_lines">2</integer>
<!-- Whether to use the split 2-column notification shade -->
<bool name="config_use_split_notification_shade">false</bool>

View File

@@ -807,6 +807,7 @@
<!-- The width/height of the unlock icon view on keyguard. -->
<dimen name="keyguard_lock_height">42dp</dimen>
<dimen name="keyguard_lock_padding">20dp</dimen>
<dimen name="secondary_message_padding">8dp</dimen>
<dimen name="keyguard_security_container_padding_top">20dp</dimen>

View File

@@ -22,8 +22,6 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import com.android.app.animation.Interpolators
@@ -41,12 +39,28 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
protected open val SHOW_DURATION_MILLIS = 150L
protected open val HIDE_DURATION_MILLIS = 200L
override fun onFinishInflate() {
super.onFinishInflate()
mDefaultColorState = getColorInStyle()
}
private fun getColorInStyle(): ColorStateList? {
val styledAttributes =
context.obtainStyledAttributes(styleResId, intArrayOf(android.R.attr.textColor))
var colorStateList: ColorStateList? = null
if (styledAttributes != null) {
colorStateList = styledAttributes.getColorStateList(0)
}
styledAttributes.recycle()
return colorStateList
}
override fun updateTextColor() {
var colorState = mDefaultColorState
mNextMessageColorState?.defaultColor?.let { color ->
if (color != DEFAULT_COLOR) {
colorState = mNextMessageColorState
mNextMessageColorState = ColorStateList.valueOf(DEFAULT_COLOR)
mNextMessageColorState = mDefaultColorState ?: ColorStateList.valueOf(DEFAULT_COLOR)
}
}
setTextColor(colorState)
@@ -57,15 +71,12 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
}
override fun onThemeChanged() {
val array: TypedArray = mContext.obtainStyledAttributes(intArrayOf(TITLE))
val newTextColors: ColorStateList = ColorStateList.valueOf(array.getColor(0, Color.RED))
array.recycle()
mDefaultColorState = newTextColors
mDefaultColorState = getColorInStyle() ?: Utils.getColorAttr(context, TITLE)
super.onThemeChanged()
}
override fun reloadColor() {
mDefaultColorState = Utils.getColorAttr(context, TITLE)
mDefaultColorState = getColorInStyle() ?: Utils.getColorAttr(context, TITLE)
super.reloadColor()
}

View File

@@ -22,6 +22,8 @@ import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.CallSuper;
import com.android.internal.widget.LockscreenCredential;
import com.android.systemui.R;
@@ -48,7 +50,9 @@ public abstract class KeyguardAbsKeyInputView extends KeyguardInputView {
protected abstract void resetState();
@Override
@CallSuper
protected void onFinishInflate() {
super.onFinishInflate();
mEcaView = findViewById(R.id.keyguard_selector_fade_container);
}

View File

@@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import java.util.HashMap;
import java.util.Map;
@@ -77,9 +78,10 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey
KeyguardSecurityCallback keyguardSecurityCallback,
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
LatencyTracker latencyTracker, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController) {
EmergencyButtonController emergencyButtonController,
FeatureFlags featureFlags) {
super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
messageAreaControllerFactory);
messageAreaControllerFactory, featureFlags);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;

View File

@@ -21,11 +21,14 @@ import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.R;
/**
* A Base class for all Keyguard password/pattern/pin related inputs.
@@ -33,6 +36,9 @@ import com.android.internal.jank.InteractionJankMonitor;
public abstract class KeyguardInputView extends LinearLayout {
private Runnable mOnFinishImeAnimationRunnable;
@Nullable
private View mBouncerMessageView;
public KeyguardInputView(Context context) {
super(context);
}
@@ -87,6 +93,18 @@ public abstract class KeyguardInputView extends LinearLayout {
mOnFinishImeAnimationRunnable = onFinishImeAnimationRunnable;
}
@Override
@CallSuper
protected void onFinishInflate() {
super.onFinishInflate();
mBouncerMessageView = findViewById(R.id.bouncer_message_view);
}
@Nullable
public final View getBouncerMessageView() {
return mBouncerMessageView;
}
public void runOnFinishImeAnimationRunnable() {
if (mOnFinishImeAnimationRunnable != null) {
mOnFinishImeAnimationRunnable.run();

View File

@@ -16,6 +16,7 @@
package com.android.keyguard;
import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
@@ -31,6 +32,7 @@ import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -53,16 +55,19 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
// (e.g. face unlock). This avoids unwanted asynchronous events from messing with the
// state for the current security method.
private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() {};
private final FeatureFlags mFeatureFlags;
protected KeyguardInputViewController(T view, SecurityMode securityMode,
KeyguardSecurityCallback keyguardSecurityCallback,
EmergencyButtonController emergencyButtonController,
@Nullable KeyguardMessageAreaController.Factory messageAreaControllerFactory) {
@Nullable KeyguardMessageAreaController.Factory messageAreaControllerFactory,
FeatureFlags featureFlags) {
super(view);
mSecurityMode = securityMode;
mKeyguardSecurityCallback = keyguardSecurityCallback;
mEmergencyButton = view == null ? null : view.findViewById(R.id.emergency_call_button);
mEmergencyButtonController = emergencyButtonController;
mFeatureFlags = featureFlags;
if (messageAreaControllerFactory != null) {
try {
BouncerKeyguardMessageArea kma = view.requireViewById(R.id.bouncer_message_area);
@@ -82,9 +87,21 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
}
@Override
@CallSuper
protected void onViewAttached() {
updateMessageAreaVisibility();
}
private void updateMessageAreaVisibility() {
if (mMessageAreaController == null) return;
if (mFeatureFlags.isEnabled(Flags.REVAMPED_BOUNCER_MESSAGES)) {
mMessageAreaController.disable();
} else {
mMessageAreaController.setIsVisible(true);
}
}
@Override
protected void onViewDetached() {
}
@@ -208,14 +225,14 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
emergencyButtonController, mMessageAreaControllerFactory,
mDevicePostureController);
mDevicePostureController, mFeatureFlags);
} else if (keyguardInputView instanceof KeyguardPasswordView) {
return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mInputMethodManager, emergencyButtonController, mMainExecutor, mResources,
mFalsingCollector, mKeyguardViewController);
mFalsingCollector, mKeyguardViewController,
mFeatureFlags);
} else if (keyguardInputView instanceof KeyguardPINView) {
return new KeyguardPinViewController((KeyguardPINView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
@@ -227,13 +244,13 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController);
emergencyButtonController, mFeatureFlags);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController);
emergencyButtonController, mFeatureFlags);
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);

View File

@@ -44,11 +44,18 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
private ViewGroup mContainer;
private int mTopMargin;
protected boolean mAnimate;
private final int mStyleResId;
private boolean mIsDisabled = false;
public KeyguardMessageArea(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug
if (attrs != null) {
mStyleResId = attrs.getStyleAttribute();
} else {
// Set to default reference style if the component is used without setting "style" attr
mStyleResId = R.style.Keyguard_TextView;
}
onThemeChanged();
}
@@ -82,13 +89,17 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
}
void onDensityOrFontScaleChanged() {
TypedArray array = mContext.obtainStyledAttributes(R.style.Keyguard_TextView, new int[] {
TypedArray array = mContext.obtainStyledAttributes(getStyleResId(), new int[] {
android.R.attr.textSize
});
setTextSize(TypedValue.COMPLEX_UNIT_PX, array.getDimensionPixelSize(0, 0));
array.recycle();
}
protected int getStyleResId() {
return mStyleResId;
}
@Override
public void setMessage(CharSequence msg, boolean animate) {
if (!TextUtils.isEmpty(msg)) {
@@ -118,6 +129,10 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
}
void update() {
if (mIsDisabled) {
setVisibility(GONE);
return;
}
CharSequence status = mMessage;
setVisibility(TextUtils.isEmpty(status) || (!mIsVisible) ? INVISIBLE : VISIBLE);
setText(status);
@@ -136,4 +151,17 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
/** Set the text color */
protected abstract void updateTextColor();
/**
* Mark this view with {@link android.view.View#GONE} visibility to remove this from the layout
* of the view. Any calls to {@link #setIsVisible(boolean)} after this will be a no-op.
*/
public void disable() {
mIsDisabled = true;
update();
}
public boolean isDisabled() {
return mIsDisabled;
}
}

View File

@@ -133,6 +133,14 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
mView.setIsVisible(isVisible);
}
/**
* Mark this view with {@link View#GONE} visibility to remove this from the layout of the view.
* Any calls to {@link #setIsVisible(boolean)} after this will be a no-op.
*/
public void disable() {
mView.disable();
}
public void setMessage(CharSequence s) {
setMessage(s, true);
}
@@ -141,6 +149,9 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
* Sets a message to the underlying text view.
*/
public void setMessage(CharSequence s, boolean animate) {
if (mView.isDisabled()) {
return;
}
mView.setMessage(s, animate);
}

View File

@@ -51,7 +51,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
private View[][] mViews;
private int mYTrans;
private int mYTransOffset;
private View mBouncerMessageView;
private View mBouncerMessageArea;
@DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
public static final long ANIMATION_DURATION = 650;
@@ -145,7 +145,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
super.onFinishInflate();
mContainer = findViewById(R.id.pin_container);
mBouncerMessageView = findViewById(R.id.bouncer_message_area);
mBouncerMessageArea = findViewById(R.id.bouncer_message_area);
mViews = new View[][]{
new View[]{
findViewById(R.id.row0), null, null
@@ -221,9 +221,9 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE;
float standardProgress = standardDecelerate.getInterpolation(progress);
mBouncerMessageView.setTranslationY(
mBouncerMessageArea.setTranslationY(
mYTrans - mYTrans * standardProgress);
mBouncerMessageView.setAlpha(standardProgress);
mBouncerMessageArea.setAlpha(standardProgress);
for (int i = 0; i < mViews.length; i++) {
View[] row = mViews[i];

View File

@@ -40,6 +40,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.util.List;
@@ -104,10 +105,11 @@ public class KeyguardPasswordViewController
@Main DelayableExecutor mainExecutor,
@Main Resources resources,
FalsingCollector falsingCollector,
KeyguardViewController keyguardViewController) {
KeyguardViewController keyguardViewController,
FeatureFlags featureFlags) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController);
emergencyButtonController, featureFlags);
mKeyguardSecurityCallback = keyguardSecurityCallback;
mInputMethodManager = inputMethodManager;
mMainExecutor = mainExecutor;

View File

@@ -38,6 +38,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.statusbar.policy.DevicePostureController;
import java.util.HashMap;
@@ -196,9 +197,9 @@ public class KeyguardPatternViewController
FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController,
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
DevicePostureController postureController) {
DevicePostureController postureController, FeatureFlags featureFlags) {
super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
messageAreaControllerFactory);
messageAreaControllerFactory, featureFlags);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;

View File

@@ -34,6 +34,8 @@ import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.CallSuper;
import com.android.app.animation.Interpolators;
import com.android.internal.widget.LockscreenCredential;
import com.android.systemui.R;
@@ -147,7 +149,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView
}
@Override
@CallSuper
protected void onFinishInflate() {
super.onFinishInflate();
mPasswordEntry = findViewById(getPasswordTextViewId());
// Set selected property on so the view can send accessibility events.

View File

@@ -27,6 +27,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView>
extends KeyguardAbsKeyInputViewController<T> {
@@ -58,10 +59,11 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
LatencyTracker latencyTracker,
LiftToActivateListener liftToActivateListener,
EmergencyButtonController emergencyButtonController,
FalsingCollector falsingCollector) {
FalsingCollector falsingCollector,
FeatureFlags featureFlags) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController);
emergencyButtonController, featureFlags);
mLiftToActivateListener = liftToActivateListener;
mFalsingCollector = falsingCollector;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());

View File

@@ -56,7 +56,7 @@ public class KeyguardPinViewController
FeatureFlags featureFlags) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector);
emergencyButtonController, falsingCollector, featureFlags);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPostureController = postureController;
mLockPatternUtils = lockPatternUtils;

View File

@@ -30,6 +30,7 @@ abstract class KeyguardSimInputView(context: Context, attrs: AttributeSet) :
private var disableESimButton: KeyguardEsimArea? = null
override fun onFinishInflate() {
super.onFinishInflate()
simImageView = findViewById(R.id.keyguard_sim)
disableESimButton = findViewById(R.id.keyguard_esim_area)
super.onFinishInflate()

View File

@@ -41,6 +41,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
public class KeyguardSimPinViewController
extends KeyguardPinBasedInputViewController<KeyguardSimPinView> {
@@ -81,10 +82,10 @@ public class KeyguardSimPinViewController
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController) {
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector);
emergencyButtonController, falsingCollector, featureFlags);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);

View File

@@ -38,6 +38,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
public class KeyguardSimPukViewController
extends KeyguardPinBasedInputViewController<KeyguardSimPukView> {
@@ -85,10 +86,10 @@ public class KeyguardSimPukViewController
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController) {
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector);
emergencyButtonController, falsingCollector, featureFlags);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);

View File

@@ -2635,6 +2635,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
return mAuthController.isUdfpsSupported();
}
/**
* @return true if the FP sensor is non-UDFPS and the device can be unlocked using fingerprint
* at this moment.
*/
public boolean isFingerprintAllowedInBouncer() {
return !isUdfpsSupported() && isUnlockingWithFingerprintAllowed();
}
/**
* @return true if there's at least one sfps enrollment for the current user.
*/

View File

@@ -14,9 +14,10 @@
* limitations under the License.
*/
package com.android.keyguard
package com.android.systemui.keyguard.bouncer.data.factory
import android.annotation.IntDef
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
@@ -34,6 +35,7 @@ import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R.string.bouncer_face_not_recognized
import com.android.systemui.R.string.keyguard_enter_password
import com.android.systemui.R.string.keyguard_enter_pattern
@@ -71,10 +73,86 @@ import com.android.systemui.R.string.kg_wrong_input_try_fp_suggestion
import com.android.systemui.R.string.kg_wrong_password_try_again
import com.android.systemui.R.string.kg_wrong_pattern_try_again
import com.android.systemui.R.string.kg_wrong_pin_try_again
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.keyguard.bouncer.shared.model.Message
import javax.inject.Inject
typealias BouncerMessage = Pair<Int, Int>
@SysUISingleton
class BouncerMessageFactory
@Inject
constructor(
private val updateMonitor: KeyguardUpdateMonitor,
private val securityModel: KeyguardSecurityModel,
) {
fun emptyBouncerMessage(): BouncerMessage = Pair(0, 0)
fun createFromPromptReason(
@BouncerPromptReason reason: Int,
userId: Int,
): BouncerMessageModel? {
val pair =
getBouncerMessage(
reason,
securityModel.getSecurityMode(userId),
updateMonitor.isFingerprintAllowedInBouncer
)
return pair?.let {
BouncerMessageModel(
message = Message(messageResId = pair.first),
secondaryMessage = Message(messageResId = pair.second)
)
}
}
fun createFromString(
primaryMsg: String? = null,
secondaryMsg: String? = null
): BouncerMessageModel =
BouncerMessageModel(
message = primaryMsg?.let { Message(message = it) },
secondaryMessage = secondaryMsg?.let { Message(message = it) },
)
/**
* Helper method that provides the relevant bouncer message that should be shown for different
* scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
* provide a more specific message.
*/
private fun getBouncerMessage(
@BouncerPromptReason reason: Int,
securityMode: SecurityMode,
fpAllowedInBouncer: Boolean = false
): Pair<Int, Int>? {
return when (reason) {
PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
else incorrectSecurityInput(securityMode)
PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
else nonStrongAuthTimeout(securityMode)
PROMPT_REASON_TRUSTAGENT_EXPIRED ->
if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
else trustAgentDisabled(securityMode)
PROMPT_REASON_INCORRECT_FACE_INPUT ->
if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
else incorrectFaceInput(securityMode)
PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
PROMPT_REASON_DEFAULT ->
if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
else defaultMessage(securityMode)
PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
else -> null
}
}
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(
@@ -97,48 +175,7 @@ fun emptyBouncerMessage(): BouncerMessage = Pair(0, 0)
)
annotation class BouncerPromptReason
/**
* Helper method that provides the relevant bouncer message that should be shown for different
* scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
* provide a more specific message.
*/
@JvmOverloads
fun getBouncerMessage(
@BouncerPromptReason reason: Int,
securityMode: SecurityMode,
fpAllowedInBouncer: Boolean = false
): BouncerMessage {
return when (reason) {
PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
else incorrectSecurityInput(securityMode)
PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
else nonStrongAuthTimeout(securityMode)
PROMPT_REASON_TRUSTAGENT_EXPIRED ->
if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
else trustAgentDisabled(securityMode)
PROMPT_REASON_INCORRECT_FACE_INPUT ->
if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
else incorrectFaceInput(securityMode)
PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
PROMPT_REASON_DEFAULT ->
if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
else defaultMessage(securityMode)
PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
else -> emptyBouncerMessage()
}
}
fun defaultMessage(securityMode: SecurityMode): BouncerMessage {
private fun defaultMessage(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
SecurityMode.Password -> Pair(keyguard_enter_password, 0)
@@ -147,7 +184,7 @@ fun defaultMessage(securityMode: SecurityMode): BouncerMessage {
}
}
fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessage {
private fun defaultMessageWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0)
SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0)
@@ -156,7 +193,7 @@ fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessage {
}
}
fun incorrectSecurityInput(securityMode: SecurityMode): BouncerMessage {
private fun incorrectSecurityInput(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0)
SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0)
@@ -165,7 +202,7 @@ fun incorrectSecurityInput(securityMode: SecurityMode): BouncerMessage {
}
}
fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMessage {
private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion)
SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion)
@@ -174,7 +211,7 @@ fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMe
}
}
fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessage {
private fun incorrectFingerprintInput(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern)
SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password)
@@ -183,7 +220,7 @@ fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessage {
}
}
fun incorrectFaceInput(securityMode: SecurityMode): BouncerMessage {
private fun incorrectFaceInput(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern)
SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password)
@@ -192,7 +229,7 @@ fun incorrectFaceInput(securityMode: SecurityMode): BouncerMessage {
}
}
fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage {
private fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized)
SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized)
@@ -201,7 +238,7 @@ fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Bounce
}
}
fun biometricLockout(securityMode: SecurityMode): BouncerMessage {
private fun biometricLockout(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password)
@@ -210,7 +247,7 @@ fun biometricLockout(securityMode: SecurityMode): BouncerMessage {
}
}
fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessage {
private fun authRequiredAfterReboot(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password)
@@ -219,7 +256,7 @@ fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessage {
}
}
fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessage {
private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock)
@@ -228,7 +265,7 @@ fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessage {
}
}
fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessage {
private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern)
SecurityMode.Password ->
@@ -238,7 +275,7 @@ fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessage {
}
}
fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessage {
private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update)
@@ -247,7 +284,7 @@ fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessage
}
}
fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMessage {
private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout)
@@ -256,7 +293,7 @@ fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMess
}
}
fun nonStrongAuthTimeout(securityMode: SecurityMode): BouncerMessage {
private fun nonStrongAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout)
@@ -265,7 +302,7 @@ fun nonStrongAuthTimeout(securityMode: SecurityMode): BouncerMessage {
}
}
fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage {
private fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout)
SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout)
@@ -274,7 +311,7 @@ fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Boun
}
}
fun faceUnlockUnavailable(securityMode: SecurityMode): BouncerMessage {
private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out)
@@ -283,7 +320,7 @@ fun faceUnlockUnavailable(securityMode: SecurityMode): BouncerMessage {
}
}
fun fingerprintUnlockUnavailable(securityMode: SecurityMode): BouncerMessage {
private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_fp_locked_out)
@@ -292,7 +329,7 @@ fun fingerprintUnlockUnavailable(securityMode: SecurityMode): BouncerMessage {
}
}
fun trustAgentDisabled(securityMode: SecurityMode): BouncerMessage {
private fun trustAgentDisabled(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled)
@@ -301,7 +338,7 @@ fun trustAgentDisabled(securityMode: SecurityMode): BouncerMessage {
}
}
fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage {
private fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled)
SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled)
@@ -310,7 +347,7 @@ fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Bounce
}
}
fun primaryAuthLockedOut(securityMode: SecurityMode): BouncerMessage {
private fun primaryAuthLockedOut(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern ->
Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern)

View File

@@ -0,0 +1,332 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.data.repository
import android.hardware.biometrics.BiometricSourceType
import android.hardware.biometrics.BiometricSourceType.FACE
import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.user.data.repository.UserRepository
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** Provide different sources of messages that needs to be shown on the bouncer. */
interface BouncerMessageRepository {
/**
* Messages that are shown in response to the incorrect security attempts on the bouncer and
* primary authentication method being locked out, along with countdown messages before primary
* auth is active again.
*/
val primaryAuthMessage: Flow<BouncerMessageModel?>
/**
* Help messages that are shown to the user on how to successfully perform authentication using
* face.
*/
val faceAcquisitionMessage: Flow<BouncerMessageModel?>
/**
* Help messages that are shown to the user on how to successfully perform authentication using
* fingerprint.
*/
val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?>
/** Custom message that is displayed when the bouncer is being shown to launch an app. */
val customMessage: Flow<BouncerMessageModel?>
/**
* Messages that are shown in response to biometric authentication attempts through face or
* fingerprint.
*/
val biometricAuthMessage: Flow<BouncerMessageModel?>
/** Messages that are shown when certain auth flags are set. */
val authFlagsMessage: Flow<BouncerMessageModel?>
/** Messages that are show after biometrics are locked out temporarily or permanently */
val biometricLockedOutMessage: Flow<BouncerMessageModel?>
/** Set the value for [primaryAuthMessage] */
fun setPrimaryAuthMessage(value: BouncerMessageModel?)
/** Set the value for [faceAcquisitionMessage] */
fun setFaceAcquisitionMessage(value: BouncerMessageModel?)
/** Set the value for [fingerprintAcquisitionMessage] */
fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?)
/** Set the value for [customMessage] */
fun setCustomMessage(value: BouncerMessageModel?)
/**
* Clear any previously set messages for [primaryAuthMessage], [faceAcquisitionMessage],
* [fingerprintAcquisitionMessage] & [customMessage]
*/
fun clearMessage()
}
@SysUISingleton
class BouncerMessageRepositoryImpl
@Inject
constructor(
trustRepository: TrustRepository,
biometricSettingsRepository: BiometricSettingsRepository,
updateMonitor: KeyguardUpdateMonitor,
private val bouncerMessageFactory: BouncerMessageFactory,
private val userRepository: UserRepository,
fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
) : BouncerMessageRepository {
private val isFaceEnrolledAndEnabled =
and(
biometricSettingsRepository.isFaceAuthenticationEnabled,
biometricSettingsRepository.isFaceEnrolled
)
private val isFingerprintEnrolledAndEnabled =
and(
biometricSettingsRepository.isFingerprintEnabledByDevicePolicy,
biometricSettingsRepository.isFingerprintEnrolled
)
private val isAnyBiometricsEnabledAndEnrolled =
or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
private val authFlagsBasedPromptReason: Flow<Int> =
combine(
biometricSettingsRepository.authenticationFlags,
trustRepository.isCurrentUserTrustManaged,
isAnyBiometricsEnabledAndEnrolled,
::Triple
)
.map { (flags, isTrustManaged, biometricsEnrolledAndEnabled) ->
val trustOrBiometricsAvailable = (isTrustManaged || biometricsEnrolledAndEnabled)
return@map if (
trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
) {
PROMPT_REASON_RESTART
} else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
PROMPT_REASON_TIMEOUT
} else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
PROMPT_REASON_DEVICE_ADMIN
} else if (isTrustManaged && flags.someAuthRequiredAfterUserRequest) {
PROMPT_REASON_TRUSTAGENT_EXPIRED
} else if (isTrustManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
PROMPT_REASON_TRUSTAGENT_EXPIRED
} else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
PROMPT_REASON_USER_REQUEST
} else if (
trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate
) {
PROMPT_REASON_PREPARE_FOR_UPDATE
} else if (
trustOrBiometricsAvailable &&
flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout
) {
PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
} else {
PROMPT_REASON_NONE
}
}
private val biometricAuthReason: Flow<Int> =
conflatedCallbackFlow {
val callback =
object : KeyguardUpdateMonitorCallback() {
override fun onBiometricAuthFailed(
biometricSourceType: BiometricSourceType?
) {
val promptReason =
if (biometricSourceType == FINGERPRINT)
PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
else if (
biometricSourceType == FACE && !updateMonitor.isFaceLockedOut
) {
PROMPT_REASON_INCORRECT_FACE_INPUT
} else PROMPT_REASON_NONE
trySendWithFailureLogging(promptReason, TAG, "onBiometricAuthFailed")
}
override fun onBiometricsCleared() {
trySendWithFailureLogging(
PROMPT_REASON_NONE,
TAG,
"onBiometricsCleared"
)
}
override fun onBiometricAcquired(
biometricSourceType: BiometricSourceType?,
acquireInfo: Int
) {
trySendWithFailureLogging(
PROMPT_REASON_NONE,
TAG,
"clearBiometricPrompt for new auth session."
)
}
override fun onBiometricAuthenticated(
userId: Int,
biometricSourceType: BiometricSourceType?,
isStrongBiometric: Boolean
) {
trySendWithFailureLogging(
PROMPT_REASON_NONE,
TAG,
"onBiometricAuthenticated"
)
}
}
updateMonitor.registerCallback(callback)
awaitClose { updateMonitor.removeCallback(callback) }
}
.distinctUntilChanged()
private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val primaryAuthMessage: Flow<BouncerMessageModel?> = _primaryAuthMessage
private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val faceAcquisitionMessage: Flow<BouncerMessageModel?> = _faceAcquisitionMessage
private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> =
_fingerprintAcquisitionMessage
private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val customMessage: Flow<BouncerMessageModel?> = _customMessage
override val biometricAuthMessage: Flow<BouncerMessageModel?> =
biometricAuthReason
.map {
if (it == PROMPT_REASON_NONE) null
else
bouncerMessageFactory.createFromPromptReason(
it,
userRepository.getSelectedUserInfo().id
)
}
.onStart { emit(null) }
.distinctUntilChanged()
override val authFlagsMessage: Flow<BouncerMessageModel?> =
authFlagsBasedPromptReason
.map {
if (it == PROMPT_REASON_NONE) null
else
bouncerMessageFactory.createFromPromptReason(
it,
userRepository.getSelectedUserInfo().id
)
}
.onStart { emit(null) }
.distinctUntilChanged()
// TODO (b/262838215): Replace with DeviceEntryFaceAuthRepository when the new face auth system
// has been launched.
private val faceLockedOut: Flow<Boolean> = conflatedCallbackFlow {
val callback =
object : KeyguardUpdateMonitorCallback() {
override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType?) {
if (biometricSourceType == FACE) {
trySendWithFailureLogging(
updateMonitor.isFaceLockedOut,
TAG,
"face lock out state changed."
)
}
}
}
updateMonitor.registerCallback(callback)
trySendWithFailureLogging(updateMonitor.isFaceLockedOut, TAG, "face lockout initial value")
awaitClose { updateMonitor.removeCallback(callback) }
}
override val biometricLockedOutMessage: Flow<BouncerMessageModel?> =
combine(fingerprintAuthRepository.isLockedOut, faceLockedOut) { fp, face ->
return@combine if (fp) {
bouncerMessageFactory.createFromPromptReason(
PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
userRepository.getSelectedUserInfo().id
)
} else if (face) {
bouncerMessageFactory.createFromPromptReason(
PROMPT_REASON_FACE_LOCKED_OUT,
userRepository.getSelectedUserInfo().id
)
} else null
}
override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
_primaryAuthMessage.value = value
}
override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
_faceAcquisitionMessage.value = value
}
override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
_fingerprintAcquisitionMessage.value = value
}
override fun setCustomMessage(value: BouncerMessageModel?) {
_customMessage.value = value
}
override fun clearMessage() {
_fingerprintAcquisitionMessage.value = null
_faceAcquisitionMessage.value = null
_primaryAuthMessage.value = null
_customMessage.value = null
}
companion object {
const val TAG = "BouncerDetailedMessageRepository"
}
}
private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
flow.combine(anotherFlow) { a, b -> a && b }
private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
flow.combine(anotherFlow) { a, b -> a || b }

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.domain.interactor
import android.os.Build
import android.util.Log
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
private val TAG = BouncerMessageAuditLogger::class.simpleName!!
/** Logger that echoes bouncer messages state to logcat in debuggable builds. */
@SysUISingleton
class BouncerMessageAuditLogger
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val repository: BouncerMessageRepository,
private val interactor: BouncerMessageInteractor,
) : CoreStartable {
override fun start() {
if (Build.isDebuggable()) {
collectAndLog(repository.biometricAuthMessage, "biometricMessage: ")
collectAndLog(repository.primaryAuthMessage, "primaryAuthMessage: ")
collectAndLog(repository.customMessage, "customMessage: ")
collectAndLog(repository.faceAcquisitionMessage, "faceAcquisitionMessage: ")
collectAndLog(
repository.fingerprintAcquisitionMessage,
"fingerprintAcquisitionMessage: "
)
collectAndLog(repository.authFlagsMessage, "authFlagsMessage: ")
collectAndLog(interactor.bouncerMessage, "interactor.bouncerMessage: ")
}
}
private fun collectAndLog(flow: Flow<BouncerMessageModel?>, context: String) {
scope.launch { flow.collect { Log.d(TAG, context + it) } }
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.domain.interactor
import android.os.CountDownTimer
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.user.data.repository.UserRepository
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@SysUISingleton
class BouncerMessageInteractor
@Inject
constructor(
private val repository: BouncerMessageRepository,
private val factory: BouncerMessageFactory,
private val userRepository: UserRepository,
private val countDownTimerUtil: CountDownTimerUtil,
private val featureFlags: FeatureFlags,
) {
fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
val callback =
object : CountDownTimerCallback {
override fun onFinish() {
repository.clearMessage()
}
override fun onTick(millisUntilFinished: Long) {
val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
val message =
factory.createFromPromptReason(
reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
userId = userRepository.getSelectedUserInfo().id
)
message?.message?.animate = false
message?.message?.formatterArgs =
mutableMapOf<String, Any>(Pair("count", secondsRemaining))
repository.setPrimaryAuthMessage(message)
}
}
countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback)
}
fun onPrimaryAuthIncorrectAttempt() {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.setPrimaryAuthMessage(
factory.createFromPromptReason(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
userRepository.getSelectedUserInfo().id
)
)
}
fun setFingerprintAcquisitionMessage(value: String?) {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.setFingerprintAcquisitionMessage(
if (value != null) {
factory.createFromString(secondaryMsg = value)
} else {
null
}
)
}
fun setFaceAcquisitionMessage(value: String?) {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.setFaceAcquisitionMessage(
if (value != null) {
factory.createFromString(secondaryMsg = value)
} else {
null
}
)
}
fun setCustomMessage(value: String?) {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.setCustomMessage(
if (value != null) {
factory.createFromString(secondaryMsg = value)
} else {
null
}
)
}
fun onPrimaryBouncerUserInput() {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.clearMessage()
}
fun onBouncerBeingHidden() {
if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
repository.clearMessage()
}
private fun firstNonNullMessage(
oneMessageModel: Flow<BouncerMessageModel?>,
anotherMessageModel: Flow<BouncerMessageModel?>
): Flow<BouncerMessageModel?> {
return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b }
}
// Null if feature flag is enabled which gets ignored always or empty bouncer message model that
// always maps to an empty string.
private fun nullOrEmptyMessage() =
flowOf(
if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
else factory.createFromString("", "")
)
val bouncerMessage =
listOf(
nullOrEmptyMessage(),
repository.primaryAuthMessage,
repository.biometricAuthMessage,
repository.fingerprintAcquisitionMessage,
repository.faceAcquisitionMessage,
repository.customMessage,
repository.authFlagsMessage,
repository.biometricLockedOutMessage,
userRepository.selectedUserInfo.map {
factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id)
},
)
.reduce(::firstNonNullMessage)
.distinctUntilChanged()
}
interface CountDownTimerCallback {
fun onFinish()
fun onTick(millisUntilFinished: Long)
}
@SysUISingleton
open class CountDownTimerUtil @Inject constructor() {
/**
* Start a new count down timer that runs for [millisInFuture] with a tick every
* [millisInterval]
*/
fun startNewTimer(
millisInFuture: Long,
millisInterval: Long,
callback: CountDownTimerCallback,
): CountDownTimer {
return object : CountDownTimer(millisInFuture, millisInterval) {
override fun onFinish() = callback.onFinish()
override fun onTick(millisUntilFinished: Long) =
callback.onTick(millisUntilFinished)
}
.start()
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.shared.model
import android.content.res.ColorStateList
/**
* Represents the message displayed on the bouncer. It has two parts, primary and a secondary
* message
*/
data class BouncerMessageModel(val message: Message? = null, val secondaryMessage: Message? = null)
/**
* Representation of a single message on the bouncer. It can be either a string or a string resource
* ID
*/
data class Message(
val message: String? = null,
val messageResId: Int? = null,
val colorState: ColorStateList? = null,
/** Any plural formatter arguments that can used to format the [messageResId] */
var formatterArgs: Map<String, Any>? = null,
/** Specifies whether this text should be animated when it is shown. */
var animate: Boolean = true,
)

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.ui
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import com.android.keyguard.BouncerKeyguardMessageArea
import com.android.keyguard.KeyguardMessageArea
import com.android.keyguard.KeyguardMessageAreaController
import com.android.systemui.R
class BouncerMessageView : LinearLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
init {
inflate(context, R.layout.bouncer_message_view, this)
}
var primaryMessageView: BouncerKeyguardMessageArea? = null
var secondaryMessageView: BouncerKeyguardMessageArea? = null
var primaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
var secondaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
override fun onFinishInflate() {
super.onFinishInflate()
primaryMessageView = findViewById(R.id.bouncer_primary_message_area)
secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area)
}
fun init(factory: KeyguardMessageAreaController.Factory) {
primaryMessage = factory.create(primaryMessageView)
primaryMessage?.init()
secondaryMessage = factory.create(secondaryMessageView)
secondaryMessage?.init()
}
}

View File

@@ -25,7 +25,6 @@ import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
import android.os.UserHandle
import android.util.Log
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.AuthController
@@ -36,13 +35,14 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.TAG
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.user.data.repository.UserRepository
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -108,10 +108,14 @@ interface BiometricSettingsRepository {
* lockdown.
*/
val isCurrentUserInLockdown: Flow<Boolean>
/** Authentication flags set for the current user. */
val authenticationFlags: Flow<AuthenticationFlags>
}
const val TAG = "BiometricsRepositoryImpl"
private const val TAG = "BiometricsRepositoryImpl"
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class BiometricSettingsRepositoryImpl
@Inject
@@ -129,6 +133,8 @@ constructor(
dumpManager: DumpManager,
) : BiometricSettingsRepository, Dumpable {
private val biometricsEnabledForUser = mutableMapOf<Int, Boolean>()
override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
private val strongAuthTracker = StrongAuthTracker(userRepository, context)
@@ -136,6 +142,9 @@ constructor(
override val isCurrentUserInLockdown: Flow<Boolean> =
strongAuthTracker.currentUserAuthFlags.map { it.isInUserLockdown }
override val authenticationFlags: Flow<AuthenticationFlags> =
strongAuthTracker.currentUserAuthFlags
init {
Log.d(TAG, "Registering StrongAuthTracker")
lockPatternUtils.registerStrongAuthTracker(strongAuthTracker)
@@ -231,9 +240,14 @@ constructor(
}
}
private val isFaceEnabledByBiometricsManagerForCurrentUser: Flow<Boolean> =
userRepository.selectedUserInfo.flatMapLatest { userInfo ->
isFaceEnabledByBiometricsManager.map { biometricsEnabledForUser[userInfo.id] ?: false }
}
override val isFaceAuthenticationEnabled: Flow<Boolean>
get() =
combine(isFaceEnabledByBiometricsManager, isFaceEnabledByDevicePolicy) {
combine(isFaceEnabledByBiometricsManagerForCurrentUser, isFaceEnabledByDevicePolicy) {
biometricsManagerSetting,
devicePolicySetting ->
biometricsManagerSetting && devicePolicySetting
@@ -249,13 +263,13 @@ constructor(
.flowOn(backgroundDispatcher)
.distinctUntilChanged()
private val isFaceEnabledByBiometricsManager =
private val isFaceEnabledByBiometricsManager: Flow<Pair<Int, Boolean>> =
conflatedCallbackFlow {
val callback =
object : IBiometricEnabledOnKeyguardCallback.Stub() {
override fun onChanged(enabled: Boolean, userId: Int) {
trySendWithFailureLogging(
enabled,
Pair(userId, enabled),
TAG,
"biometricsEnabled state changed"
)
@@ -264,9 +278,10 @@ constructor(
biometricManager?.registerEnabledOnKeyguardCallback(callback)
awaitClose {}
}
.onEach { biometricsEnabledForUser[it.first] = it.second }
// This is because the callback is binder-based and we want to avoid multiple callbacks
// being registered.
.stateIn(scope, SharingStarted.Eagerly, false)
.stateIn(scope, SharingStarted.Eagerly, Pair(0, false))
override val isStrongBiometricAllowed: StateFlow<Boolean> =
strongAuthTracker.isStrongBiometricAllowed.stateIn(
@@ -306,14 +321,13 @@ constructor(
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) :
LockPatternUtils.StrongAuthTracker(context) {
// Backing field for onStrongAuthRequiredChanged
private val _strongAuthFlags =
MutableStateFlow(
StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId))
)
private val _authFlags =
MutableStateFlow(AuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)))
// Backing field for onIsNonStrongBiometricAllowedChanged
private val _nonStrongBiometricAllowed =
@@ -321,17 +335,15 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont
Pair(currentUserId, isNonStrongBiometricAllowedAfterIdleTimeout(currentUserId))
)
val currentUserAuthFlags: Flow<StrongAuthenticationFlags> =
val currentUserAuthFlags: Flow<AuthenticationFlags> =
userRepository.selectedUserInfo
.map { it.id }
.distinctUntilChanged()
.flatMapLatest { userId ->
_strongAuthFlags
.filter { it.userId == userId }
_authFlags
.map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) }
.onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") }
.onStart {
emit(StrongAuthenticationFlags(userId, getStrongAuthForUser(userId)))
}
.onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) }
}
/** isStrongBiometricAllowed for the current user. */
@@ -356,7 +368,7 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont
override fun onStrongAuthRequiredChanged(userId: Int) {
val newFlags = getStrongAuthForUser(userId)
_strongAuthFlags.value = StrongAuthenticationFlags(userId, newFlags)
_authFlags.value = AuthenticationFlags(userId, newFlags)
Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags")
}
@@ -375,11 +387,3 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean =
private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean =
(getKeyguardDisabledFeatures(null, userId) and policy) == 0
private data class StrongAuthenticationFlags(val userId: Int, val flag: Int) {
val isInUserLockdown = containsFlag(flag, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN)
}
private fun containsFlag(haystack: Int, needle: Int): Boolean {
return haystack and needle != 0
}

View File

@@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.log.dagger.BouncerLog
import com.android.systemui.log.dagger.BouncerTableLog
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.util.time.SystemClock
@@ -105,7 +105,7 @@ class KeyguardBouncerRepositoryImpl
constructor(
private val clock: SystemClock,
@Application private val applicationScope: CoroutineScope,
@BouncerLog private val buffer: TableLogBuffer,
@BouncerTableLog private val buffer: TableLogBuffer,
) : KeyguardBouncerRepository {
/** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
private val _primaryBouncerShow = MutableStateFlow(false)

View File

@@ -16,8 +16,14 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.CoreStartable
import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepositoryImpl
import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageAuditLogger
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
interface KeyguardRepositoryModule {
@@ -46,5 +52,13 @@ interface KeyguardRepositoryModule {
@Binds
fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
@Binds
fun bouncerMessageRepository(impl: BouncerMessageRepositoryImpl): BouncerMessageRepository
@Binds
@IntoMap
@ClassKey(BouncerMessageAuditLogger::class)
fun bind(impl: BouncerMessageAuditLogger): CoreStartable
@Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 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.keyguard.shared.model
import com.android.internal.widget.LockPatternUtils
/** Authentication flags corresponding to a user. */
data class AuthenticationFlags(val userId: Int, val flag: Int) {
val isInUserLockdown =
containsFlag(
flag,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
)
val isPrimaryAuthRequiredAfterReboot =
containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT)
val isPrimaryAuthRequiredAfterTimeout =
containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT)
val isPrimaryAuthRequiredAfterDpmLockdown =
containsFlag(
flag,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW
)
val someAuthRequiredAfterUserRequest =
containsFlag(flag, LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST)
val someAuthRequiredAfterTrustAgentExpired =
containsFlag(
flag,
LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
)
val primaryAuthRequiredForUnattendedUpdate =
containsFlag(
flag,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
)
/** Either Class 3 biometrics or primary auth can be used to unlock the device. */
val strongerAuthRequiredAfterNonStrongBiometricsTimeout =
containsFlag(
flag,
LockPatternUtils.StrongAuthTracker
.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
)
}
private fun containsFlag(haystack: Int, needle: Int): Boolean {
return haystack and needle != 0
}

View File

@@ -38,7 +38,8 @@ data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationS
object FailedAuthenticationStatus : AuthenticationStatus()
/** Face authentication error message */
data class ErrorAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus() {
data class ErrorAuthenticationStatus(val msgId: Int, val msg: String? = null) :
AuthenticationStatus() {
/**
* Method that checks if [msgId] is a lockout error. A lockout error means that face
* authentication is locked out.

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 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.log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.log.dagger.BouncerLog
import javax.inject.Inject
private const val TAG = "BouncerLog"
/**
* Helper class for logging for classes in the [com.android.systemui.keyguard.bouncer] package.
*
* To enable logcat echoing for an entire buffer:
* ```
* adb shell settings put global systemui/buffer/BouncerLog <logLevel>
*
* ```
*/
@SysUISingleton
class BouncerLogger @Inject constructor(@BouncerLog private val buffer: LogBuffer) {
fun startBouncerMessageInteractor() {
buffer.log(
TAG,
LogLevel.DEBUG,
"Starting BouncerMessageInteractor.bouncerMessage collector"
)
}
fun bouncerMessageUpdated(bouncerMsg: BouncerMessageModel?) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
int1 = bouncerMsg?.message?.messageResId ?: -1
str1 = bouncerMsg?.message?.message
int2 = bouncerMsg?.secondaryMessage?.messageResId ?: -1
str2 = bouncerMsg?.secondaryMessage?.message
},
{ "Bouncer message update received: $int1, $str1, $int2, $str2" }
)
}
fun bindingBouncerMessageView() {
buffer.log(TAG, LogLevel.DEBUG, "Binding BouncerMessageView")
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 The Android Open Source Project
* Copyright (C) 2023 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.
@@ -16,10 +16,7 @@
package com.android.systemui.log.dagger
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier
/** Logger for the primary and alternative bouncers. */
@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerLog
/** A [com.android.systemui.log.LogBuffer] for bouncer and its child views. */
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class BouncerLog()

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2022 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.log.dagger
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier
/** Logger for the primary and alternative bouncers. */
@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerTableLog

View File

@@ -403,6 +403,17 @@ public class LogModule {
return factory.create("DeviceEntryFaceAuthRepositoryLog", 300);
}
/**
* Provides a {@link LogBuffer} for use by classes in the
* {@link com.android.systemui.keyguard.bouncer} package.
*/
@Provides
@SysUISingleton
@BouncerLog
public static LogBuffer provideBouncerLog(LogBufferFactory factory) {
return factory.create("BouncerLog", 100);
}
/**
* Provides a {@link LogBuffer} for Device State Auto-Rotation logs.
*/
@@ -426,9 +437,9 @@ public class LogModule {
/** Provides a logging buffer for the primary bouncer. */
@Provides
@SysUISingleton
@BouncerLog
@BouncerTableLog
public static TableLogBuffer provideBouncerLogBuffer(TableLogBufferFactory factory) {
return factory.create("BouncerLog", 250);
return factory.create("BouncerTableLog", 250);
}
/** Provides a table logging buffer for the Monitor. */

View File

@@ -40,6 +40,8 @@ import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -77,6 +79,7 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase {
@Mock
private EmergencyButtonController mEmergencyButtonController;
private FakeFeatureFlags mFeatureFlags;
private KeyguardAbsKeyInputViewController mKeyguardAbsKeyInputViewController;
@Before
@@ -90,10 +93,18 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase {
when(mAbsKeyInputView.requireViewById(R.id.bouncer_message_area))
.thenReturn(mKeyguardMessageArea);
when(mAbsKeyInputView.getResources()).thenReturn(getContext().getResources());
mKeyguardAbsKeyInputViewController = new KeyguardAbsKeyInputViewController(mAbsKeyInputView,
mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false);
mKeyguardAbsKeyInputViewController = createTestObject();
mKeyguardAbsKeyInputViewController.init();
reset(mKeyguardMessageAreaController); // Clear out implicit call to init.
}
private KeyguardAbsKeyInputViewController createTestObject() {
return new KeyguardAbsKeyInputViewController(mAbsKeyInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector,
mEmergencyButtonController) {
mEmergencyButtonController, mFeatureFlags) {
@Override
void resetState() {
}
@@ -108,8 +119,16 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase {
return 0;
}
};
mKeyguardAbsKeyInputViewController.init();
reset(mKeyguardMessageAreaController); // Clear out implicit call to init.
}
@Test
public void withFeatureFlagOn_oldMessage_isHidden() {
mFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true);
KeyguardAbsKeyInputViewController underTest = createTestObject();
underTest.init();
verify(mKeyguardMessageAreaController).disable();
}
@Test

View File

@@ -26,6 +26,8 @@ import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.concurrency.DelayableExecutor
import org.junit.Before
import org.junit.Test
@@ -76,6 +78,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() {
Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry))
.thenReturn(passwordEntry)
`when`(keyguardPasswordView.resources).thenReturn(context.resources)
val fakeFeatureFlags = FakeFeatureFlags()
fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
keyguardPasswordViewController =
KeyguardPasswordViewController(
keyguardPasswordView,
@@ -90,7 +94,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() {
mainExecutor,
mContext.resources,
falsingCollector,
keyguardViewController)
keyguardViewController,
fakeFeatureFlags)
}
@Test

View File

@@ -26,6 +26,8 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.policy.DevicePostureController
import org.junit.Before
import org.junit.Test
@@ -72,6 +74,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
@Mock private lateinit var mPostureController: DevicePostureController
private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
private lateinit var fakeFeatureFlags: FakeFeatureFlags
@Before
fun setup() {
@@ -86,6 +89,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
`when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
.thenReturn(mKeyguardMessageAreaController)
`when`(mKeyguardPatternView.resources).thenReturn(context.resources)
fakeFeatureFlags = FakeFeatureFlags()
fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false)
mKeyguardPatternViewController =
KeyguardPatternViewController(
mKeyguardPatternView,
@@ -97,7 +102,17 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
mFalsingCollector,
mEmergencyButtonController,
mKeyguardMessageAreaControllerFactory,
mPostureController)
mPostureController,
fakeFeatureFlags)
}
@Test
fun withFeatureFlagOn_oldMessage_isHidden() {
fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
mKeyguardPatternViewController.init()
verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable()
}
@Test

View File

@@ -36,6 +36,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.SingleTapClassifier;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -98,10 +100,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
.thenReturn(mDeleteButton);
when(mPinBasedInputView.findViewById(R.id.key_enter))
.thenReturn(mOkButton);
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true);
mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener,
mEmergencyButtonController, mFalsingCollector) {
mEmergencyButtonController, mFalsingCollector, featureFlags) {
@Override
public void onResume(int reason) {
super.onResume(reason);

View File

@@ -63,7 +63,9 @@ import com.android.systemui.biometrics.SideFpsController;
import com.android.systemui.biometrics.SideFpsUiRequestSource;
import com.android.systemui.classifier.FalsingA11yDelegate;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
@@ -195,11 +197,15 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
when(mKeyguardPasswordView.getWindowInsetsController()).thenReturn(mWindowInsetsController);
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true);
mKeyguardPasswordViewController = new KeyguardPasswordViewController(
(KeyguardPasswordView) mKeyguardPasswordView, mKeyguardUpdateMonitor,
SecurityMode.Password, mLockPatternUtils, null,
mKeyguardMessageAreaControllerFactory, null, null, mEmergencyButtonController,
null, mock(Resources.class), null, mKeyguardViewController);
null, mock(Resources.class), null, mKeyguardViewController,
featureFlags);
mKeyguardSecurityContainerController = new KeyguardSecurityContainerController(
mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,

View File

@@ -27,6 +27,8 @@ import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Test
@@ -71,6 +73,9 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
simPinView =
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null)
as KeyguardSimPinView
val fakeFeatureFlags = FakeFeatureFlags()
fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
underTest =
KeyguardSimPinViewController(
simPinView,
@@ -83,7 +88,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
liftToActivateListener,
telephonyManager,
falsingCollector,
emergencyButtonController
emergencyButtonController,
fakeFeatureFlags,
)
underTest.init()
}

View File

@@ -27,6 +27,8 @@ import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Test
@@ -70,6 +72,9 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
simPukView =
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_puk_view, null)
as KeyguardSimPukView
val fakeFeatureFlags = FakeFeatureFlags()
fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
underTest =
KeyguardSimPukViewController(
simPukView,
@@ -82,7 +87,8 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
liftToActivateListener,
telephonyManager,
falsingCollector,
emergencyButtonController
emergencyButtonController,
fakeFeatureFlags,
)
underTest.init()
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.data.factory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.StringSubject
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class BouncerMessageFactoryTest : SysuiTestCase() {
private lateinit var underTest: BouncerMessageFactory
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var securityModel: KeyguardSecurityModel
private lateinit var testScope: TestScope
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testScope = TestScope()
underTest = BouncerMessageFactory(updateMonitor, securityModel)
}
@Test
fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
testScope.runTest {
primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false)
.isEqualTo("Enter PIN")
primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true)
.isEqualTo("Unlock with PIN or fingerprint")
primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false)
.isEqualTo("Enter password")
primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true)
.isEqualTo("Unlock with password or fingerprint")
primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false)
.isEqualTo("Draw pattern")
primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true)
.isEqualTo("Unlock with pattern or fingerprint")
}
@Test
fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
testScope.runTest {
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = PIN,
fpAllowedInBouncer = true
)
.isEqualTo("Wrong PIN. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = PIN,
fpAllowedInBouncer = true
)
.isEqualTo("Or unlock with fingerprint")
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Password,
fpAllowedInBouncer = true
)
.isEqualTo("Wrong password. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Password,
fpAllowedInBouncer = true
)
.isEqualTo("Or unlock with fingerprint")
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Pattern,
fpAllowedInBouncer = true
)
.isEqualTo("Wrong pattern. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Pattern,
fpAllowedInBouncer = true
)
.isEqualTo("Or unlock with fingerprint")
}
private fun primaryMessage(
reason: Int,
mode: KeyguardSecurityModel.SecurityMode,
fpAllowedInBouncer: Boolean
): StringSubject {
return assertThat(
context.resources.getString(
bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!!
)
)!!
}
private fun secondaryMessage(
reason: Int,
mode: KeyguardSecurityModel.SecurityMode,
fpAllowedInBouncer: Boolean
): StringSubject {
return assertThat(
context.resources.getString(
bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!
.secondaryMessage!!
.messageResId!!
)
)!!
}
private fun bouncerMessageModel(
mode: KeyguardSecurityModel.SecurityMode,
fpAllowedInBouncer: Boolean,
reason: Int
): BouncerMessageModel? {
whenever(securityModel.getSecurityMode(0)).thenReturn(mode)
whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer)
return underTest.createFromPromptReason(reason, 0)
}
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.data.repository
import android.content.pm.UserInfo
import android.hardware.biometrics.BiometricSourceType
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.R
import com.android.systemui.R.string.keyguard_enter_pin
import com.android.systemui.R.string.kg_prompt_after_dpm_lock
import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
import com.android.systemui.R.string.kg_prompt_auth_timeout
import com.android.systemui.R.string.kg_prompt_pin_auth_timeout
import com.android.systemui.R.string.kg_prompt_reason_restart_pin
import com.android.systemui.R.string.kg_prompt_unattended_update
import com.android.systemui.R.string.kg_trust_agent_disabled
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.keyguard.bouncer.shared.model.Message
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class BouncerMessageRepositoryTest : SysuiTestCase() {
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var securityModel: KeyguardSecurityModel
@Captor
private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
private lateinit var underTest: BouncerMessageRepository
private lateinit var trustRepository: FakeTrustRepository
private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
private lateinit var userRepository: FakeUserRepository
private lateinit var fingerprintRepository: FakeDeviceEntryFingerprintAuthRepository
private lateinit var testScope: TestScope
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
trustRepository = FakeTrustRepository()
biometricSettingsRepository = FakeBiometricSettingsRepository()
userRepository = FakeUserRepository()
userRepository.setUserInfos(listOf(PRIMARY_USER))
fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
testScope = TestScope()
whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
underTest =
BouncerMessageRepositoryImpl(
trustRepository = trustRepository,
biometricSettingsRepository = biometricSettingsRepository,
updateMonitor = updateMonitor,
bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
userRepository = userRepository,
fingerprintAuthRepository = fingerprintRepository
)
}
@Test
fun setCustomMessage_propagatesState() =
testScope.runTest {
underTest.setCustomMessage(message("not empty"))
val customMessage = collectLastValue(underTest.customMessage)
assertThat(customMessage()).isEqualTo(message("not empty"))
}
@Test
fun setFaceMessage_propagatesState() =
testScope.runTest {
underTest.setFaceAcquisitionMessage(message("not empty"))
val faceAcquisitionMessage = collectLastValue(underTest.faceAcquisitionMessage)
assertThat(faceAcquisitionMessage()).isEqualTo(message("not empty"))
}
@Test
fun setFpMessage_propagatesState() =
testScope.runTest {
underTest.setFingerprintAcquisitionMessage(message("not empty"))
val fpAcquisitionMsg = collectLastValue(underTest.fingerprintAcquisitionMessage)
assertThat(fpAcquisitionMsg()).isEqualTo(message("not empty"))
}
@Test
fun setPrimaryAuthMessage_propagatesState() =
testScope.runTest {
underTest.setPrimaryAuthMessage(message("not empty"))
val primaryAuthMessage = collectLastValue(underTest.primaryAuthMessage)
assertThat(primaryAuthMessage()).isEqualTo(message("not empty"))
}
@Test
fun biometricAuthMessage_propagatesBiometricAuthMessages() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
val biometricAuthMessage = collectLastValue(underTest.biometricAuthMessage)
runCurrent()
verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT)
assertThat(biometricAuthMessage())
.isEqualTo(message(R.string.kg_fp_not_recognized, R.string.kg_bio_try_again_or_pin))
updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FACE)
assertThat(biometricAuthMessage())
.isEqualTo(
message(R.string.bouncer_face_not_recognized, R.string.kg_bio_try_again_or_pin)
)
updateMonitorCallback.value.onBiometricAcquired(BiometricSourceType.FACE, 0)
assertThat(biometricAuthMessage()).isNull()
}
@Test
fun onFaceLockout_propagatesState() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
val lockoutMessage = collectLastValue(underTest.biometricLockedOutMessage)
runCurrent()
verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
whenever(updateMonitor.isFaceLockedOut).thenReturn(true)
updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
assertThat(lockoutMessage())
.isEqualTo(message(keyguard_enter_pin, R.string.kg_face_locked_out))
whenever(updateMonitor.isFaceLockedOut).thenReturn(false)
updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
assertThat(lockoutMessage()).isNull()
}
@Test
fun onFingerprintLockout_propagatesState() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
val lockedOutMessage = collectLastValue(underTest.biometricLockedOutMessage)
runCurrent()
fingerprintRepository.setLockedOut(true)
assertThat(lockedOutMessage())
.isEqualTo(message(keyguard_enter_pin, R.string.kg_fp_locked_out))
fingerprintRepository.setLockedOut(false)
assertThat(lockedOutMessage()).isNull()
}
@Test
fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
trustRepository.setCurrentUserTrustManaged(false)
biometricSettingsRepository.setFaceEnrolled(false)
biometricSettingsRepository.setFingerprintEnrolled(false)
verifyMessagesForAuthFlag(
STRONG_AUTH_NOT_REQUIRED to null,
STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null,
STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to null,
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
)
}
@Test
fun authFlagsChanges_withTrustManaged_providesDifferentMessages() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
biometricSettingsRepository.setFaceEnrolled(false)
biometricSettingsRepository.setFingerprintEnrolled(false)
trustRepository.setCurrentUserTrustManaged(true)
verifyMessagesForAuthFlag(
STRONG_AUTH_NOT_REQUIRED to null,
STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
STRONG_AUTH_REQUIRED_AFTER_BOOT to
Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
Pair(keyguard_enter_pin, kg_trust_agent_disabled),
SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
Pair(keyguard_enter_pin, kg_trust_agent_disabled),
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
Pair(keyguard_enter_pin, kg_prompt_unattended_update),
STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
)
}
@Test
fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
trustRepository.setCurrentUserTrustManaged(false)
biometricSettingsRepository.setFingerprintEnrolled(false)
biometricSettingsRepository.setIsFaceAuthEnabled(true)
biometricSettingsRepository.setFaceEnrolled(true)
verifyMessagesForAuthFlag(
STRONG_AUTH_NOT_REQUIRED to null,
STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
STRONG_AUTH_REQUIRED_AFTER_BOOT to
Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
Pair(keyguard_enter_pin, kg_prompt_unattended_update),
STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
)
}
@Test
fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
trustRepository.setCurrentUserTrustManaged(false)
biometricSettingsRepository.setIsFaceAuthEnabled(false)
biometricSettingsRepository.setFaceEnrolled(false)
biometricSettingsRepository.setFingerprintEnrolled(true)
biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
verifyMessagesForAuthFlag(
STRONG_AUTH_NOT_REQUIRED to null,
STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
STRONG_AUTH_REQUIRED_AFTER_BOOT to
Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
Pair(keyguard_enter_pin, kg_prompt_unattended_update),
STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
)
}
private fun TestScope.verifyMessagesForAuthFlag(
vararg authFlagToExpectedMessages: Pair<Int, Pair<Int, Int>?>
) {
val authFlagsMessage = collectLastValue(underTest.authFlagsMessage)
authFlagToExpectedMessages.forEach { (flag, messagePair) ->
biometricSettingsRepository.setAuthenticationFlags(
AuthenticationFlags(PRIMARY_USER_ID, flag)
)
assertThat(authFlagsMessage())
.isEqualTo(messagePair?.let { message(it.first, it.second) })
}
}
private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
return BouncerMessageModel(
message = Message(messageResId = primaryResId),
secondaryMessage = Message(messageResId = secondaryResId)
)
}
private fun message(value: String): BouncerMessageModel {
return BouncerMessageModel(message = Message(message = value))
}
companion object {
private const val PRIMARY_USER_ID = 0
private val PRIMARY_USER =
UserInfo(
/* id= */ PRIMARY_USER_ID,
/* name= */ "primary user",
/* flags= */ UserInfo.FLAG_PRIMARY
)
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright (C) 2023 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.keyguard.bouncer.domain.interactor
import android.content.pm.UserInfo
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R.string.keyguard_enter_pin
import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.keyguard.bouncer.shared.model.Message
import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.KotlinArgumentCaptor
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class BouncerMessageInteractorTest : SysuiTestCase() {
@Mock private lateinit var securityModel: KeyguardSecurityModel
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback>
private lateinit var underTest: BouncerMessageInteractor
private lateinit var repository: FakeBouncerMessageRepository
private lateinit var userRepository: FakeUserRepository
private lateinit var testScope: TestScope
private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?>
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
repository = FakeBouncerMessageRepository()
userRepository = FakeUserRepository()
userRepository.setUserInfos(listOf(PRIMARY_USER))
testScope = TestScope()
countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
allowTestableLooperAsMainThread()
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
}
suspend fun TestScope.init() {
userRepository.setSelectedUserInfo(PRIMARY_USER)
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
underTest =
BouncerMessageInteractor(
repository = repository,
factory = BouncerMessageFactory(updateMonitor, securityModel),
userRepository = userRepository,
countDownTimerUtil = countDownTimerUtil,
featureFlags = featureFlags
)
bouncerMessage = collectLastValue(underTest.bouncerMessage)
}
@Test
fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() =
testScope.runTest {
init()
underTest.onPrimaryAuthIncorrectAttempt()
assertThat(repository.primaryAuthMessage).isNotNull()
assertThat(
context.resources.getString(
repository.primaryAuthMessage.value!!.message!!.messageResId!!
)
)
.isEqualTo("Wrong PIN. Try again.")
}
@Test
fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() =
testScope.runTest {
init()
repository.setCustomMessage(message("not empty"))
repository.setFaceAcquisitionMessage(message("not empty"))
repository.setFingerprintAcquisitionMessage(message("not empty"))
repository.setPrimaryAuthMessage(message("not empty"))
underTest.onPrimaryBouncerUserInput()
assertThat(repository.customMessage.value).isNull()
assertThat(repository.faceAcquisitionMessage.value).isNull()
assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
assertThat(repository.primaryAuthMessage.value).isNull()
}
@Test
fun onBouncerBeingHidden_clearsAllSetBouncerMessages() =
testScope.runTest {
init()
repository.setCustomMessage(message("not empty"))
repository.setFaceAcquisitionMessage(message("not empty"))
repository.setFingerprintAcquisitionMessage(message("not empty"))
repository.setPrimaryAuthMessage(message("not empty"))
underTest.onBouncerBeingHidden()
assertThat(repository.customMessage.value).isNull()
assertThat(repository.faceAcquisitionMessage.value).isNull()
assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
assertThat(repository.primaryAuthMessage.value).isNull()
}
@Test
fun setCustomMessage_setsRepositoryValue() =
testScope.runTest {
init()
underTest.setCustomMessage("not empty")
assertThat(repository.customMessage.value)
.isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
underTest.setCustomMessage(null)
assertThat(repository.customMessage.value).isNull()
}
@Test
fun setFaceMessage_setsRepositoryValue() =
testScope.runTest {
init()
underTest.setFaceAcquisitionMessage("not empty")
assertThat(repository.faceAcquisitionMessage.value)
.isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
underTest.setFaceAcquisitionMessage(null)
assertThat(repository.faceAcquisitionMessage.value).isNull()
}
@Test
fun setFingerprintMessage_setsRepositoryValue() =
testScope.runTest {
init()
underTest.setFingerprintAcquisitionMessage("not empty")
assertThat(repository.fingerprintAcquisitionMessage.value)
.isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
underTest.setFingerprintAcquisitionMessage(null)
assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
}
@Test
fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() =
testScope.runTest {
init()
underTest.onPrimaryAuthLockedOut(3)
verify(countDownTimerUtil)
.startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
countDownTimerCallback.value.onTick(2000L)
val primaryMessage = repository.primaryAuthMessage.value!!.message!!
assertThat(primaryMessage.messageResId!!)
.isEqualTo(kg_too_many_failed_attempts_countdown)
assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2)))
}
@Test
fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() =
testScope.runTest {
init()
repository.setCustomMessage(message("not empty"))
repository.setFaceAcquisitionMessage(message("not empty"))
repository.setFingerprintAcquisitionMessage(message("not empty"))
repository.setPrimaryAuthMessage(message("not empty"))
underTest.onPrimaryAuthLockedOut(3)
verify(countDownTimerUtil)
.startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
countDownTimerCallback.value.onFinish()
assertThat(repository.customMessage.value).isNull()
assertThat(repository.faceAcquisitionMessage.value).isNull()
assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
assertThat(repository.primaryAuthMessage.value).isNull()
}
@Test
fun bouncerMessage_hasPriorityOrderOfMessages() =
testScope.runTest {
init()
repository.setBiometricAuthMessage(message("biometric message"))
repository.setFaceAcquisitionMessage(message("face acquisition message"))
repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message"))
repository.setPrimaryAuthMessage(message("primary auth message"))
repository.setAuthFlagsMessage(message("auth flags message"))
repository.setBiometricLockedOutMessage(message("biometrics locked out"))
repository.setCustomMessage(message("custom message"))
assertThat(bouncerMessage()).isEqualTo(message("primary auth message"))
repository.setPrimaryAuthMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("biometric message"))
repository.setBiometricAuthMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message"))
repository.setFingerprintAcquisitionMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("face acquisition message"))
repository.setFaceAcquisitionMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("custom message"))
repository.setCustomMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("auth flags message"))
repository.setAuthFlagsMessage(null)
assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out"))
repository.setBiometricLockedOutMessage(null)
// sets the default message if everything else is null
assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
}
private fun message(value: String): BouncerMessageModel {
return BouncerMessageModel(message = Message(message = value))
}
companion object {
private const val PRIMARY_USER_ID = 0
private val PRIMARY_USER =
UserInfo(
/* id= */ PRIMARY_USER_ID,
/* name= */ "primary user",
/* flags= */ UserInfo.FLAG_PRIMARY
)
}
}

View File

@@ -310,19 +310,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
createBiometricSettingsRepository()
verify(biometricManager)
.registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
.thenReturn(0)
broadcastDPMStateChange()
biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
assertThat(isFaceAuthEnabled()).isTrue()
assertThat(isFaceAuthEnabled()).isFalse()
biometricManagerCallback.value.onChanged(false, PRIMARY_USER_ID)
// Value changes for another user
biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID)
assertThat(isFaceAuthEnabled()).isFalse()
// Value changes for current user.
biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
assertThat(isFaceAuthEnabled()).isTrue()
}
@Test
fun userChange_biometricEnabledChange_handlesRaceCondition() =
testScope.runTest {
createBiometricSettingsRepository()
verify(biometricManager)
.registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID)
runCurrent()
userRepository.setSelectedUserInfo(ANOTHER_USER)
runCurrent()
assertThat(isFaceAuthEnabled()).isTrue()
}
@Test
@@ -382,7 +401,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
}
@Test
fun userInLockdownUsesStrongAuthFlagsToDetermineValue() =
fun userInLockdownUsesAuthFlagsToDetermineValue() =
testScope.runTest {
createBiometricSettingsRepository()
@@ -405,6 +424,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
assertThat(isUserInLockdown()).isTrue()
}
@Test
fun authFlagChangesForCurrentUserArePropagated() =
testScope.runTest {
createBiometricSettingsRepository()
val authFlags = collectLastValue(underTest.authenticationFlags)
// has default value.
val defaultStrongAuthValue = STRONG_AUTH_REQUIRED_AFTER_BOOT
assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue)
// change strong auth flags for another user.
// Combine with one more flag to check if we do the bitwise and
val inLockdown =
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN or STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
onStrongAuthChanged(inLockdown, ANOTHER_USER_ID)
// Still false.
assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue)
// change strong auth flags for current user.
onStrongAuthChanged(inLockdown, PRIMARY_USER_ID)
assertThat(authFlags()!!.flag).isEqualTo(inLockdown)
onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, ANOTHER_USER_ID)
assertThat(authFlags()!!.flag).isEqualTo(inLockdown)
userRepository.setSelectedUserInfo(ANOTHER_USER)
assertThat(authFlags()!!.flag).isEqualTo(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT)
}
private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) {
authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled)
}

View File

@@ -17,10 +17,13 @@
package com.android.systemui.keyguard.data.repository
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
class FakeBiometricSettingsRepository : BiometricSettingsRepository {
@@ -50,9 +53,12 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
get() = _isFaceAuthSupportedInCurrentPosture
private val _isCurrentUserInLockdown = MutableStateFlow(false)
override val isCurrentUserInLockdown: Flow<Boolean>
get() = _isCurrentUserInLockdown
get() = _authFlags.map { it.isInUserLockdown }
private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0))
override val authenticationFlags: Flow<AuthenticationFlags>
get() = _authFlags
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
@@ -66,6 +72,10 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
_isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy
}
fun setAuthenticationFlags(value: AuthenticationFlags) {
_authFlags.value = value
}
fun setFaceEnrolled(isFaceEnrolled: Boolean) {
_isFaceEnrolled.value = isFaceEnrolled
}
@@ -79,7 +89,22 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
}
fun setIsUserInLockdown(value: Boolean) {
_isCurrentUserInLockdown.value = value
if (value) {
setAuthenticationFlags(
AuthenticationFlags(
_authFlags.value.userId,
_authFlags.value.flag or
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
)
)
} else {
setAuthenticationFlags(
AuthenticationFlags(
_authFlags.value.userId,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
)
)
}
}
fun setIsNonStrongBiometricAllowed(value: Boolean) {

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2023 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.keyguard.data.repository
import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeBouncerMessageRepository : BouncerMessageRepository {
private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val primaryAuthMessage: StateFlow<BouncerMessageModel?>
get() = _primaryAuthMessage
private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val faceAcquisitionMessage: StateFlow<BouncerMessageModel?>
get() = _faceAcquisitionMessage
private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val fingerprintAcquisitionMessage: StateFlow<BouncerMessageModel?>
get() = _fingerprintAcquisitionMessage
private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val customMessage: StateFlow<BouncerMessageModel?>
get() = _customMessage
private val _biometricAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val biometricAuthMessage: StateFlow<BouncerMessageModel?>
get() = _biometricAuthMessage
private val _authFlagsMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val authFlagsMessage: StateFlow<BouncerMessageModel?>
get() = _authFlagsMessage
private val _biometricLockedOutMessage = MutableStateFlow<BouncerMessageModel?>(null)
override val biometricLockedOutMessage: Flow<BouncerMessageModel?>
get() = _biometricLockedOutMessage
override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
_primaryAuthMessage.value = value
}
override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
_faceAcquisitionMessage.value = value
}
override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
_fingerprintAcquisitionMessage.value = value
}
override fun setCustomMessage(value: BouncerMessageModel?) {
_customMessage.value = value
}
fun setBiometricAuthMessage(value: BouncerMessageModel?) {
_biometricAuthMessage.value = value
}
fun setAuthFlagsMessage(value: BouncerMessageModel?) {
_authFlagsMessage.value = value
}
fun setBiometricLockedOutMessage(value: BouncerMessageModel?) {
_biometricLockedOutMessage.value = value
}
override fun clearMessage() {
_primaryAuthMessage.value = null
_faceAcquisitionMessage.value = null
_fingerprintAcquisitionMessage.value = null
_customMessage.value = null
}
}