diff options
| author | 2023-06-30 03:00:44 +0000 | |
|---|---|---|
| committer | 2023-06-30 03:00:44 +0000 | |
| commit | 682c51f544104a49c6f8ca373f1c74aa1184e56d (patch) | |
| tree | 83b91b02c13dedbb2af471b10b4aebf4ac477b84 | |
| parent | aaef6a2d2c25e75302e52d371cb73b078b4ebbe3 (diff) | |
| parent | cb1b8cb7fa19d8b938dfb45f1d64c046b68b2d67 (diff) | |
Merge "Modernize UDFPS views for maintainability" into udc-qpr-dev
27 files changed, 1985 insertions, 37 deletions
diff --git a/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.xml b/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.xml new file mode 100644 index 000000000000..360ef2672e75 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.xml @@ -0,0 +1,25 @@ +<?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. + --> +<com.android.systemui.biometrics.UdfpsKeyguardView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/udfps_animation_view" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- Add fingerprint views here. See udfps_keyguard_view_internal.xml. --> + +</com.android.systemui.biometrics.UdfpsKeyguardView> diff --git a/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml b/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml index 00af7f4e10e3..530d752732c1 100644 --- a/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml +++ b/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml @@ -16,8 +16,7 @@ --> <com.android.systemui.biometrics.UdfpsKeyguardViewLegacy xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/udfps_animation_view" + android:id="@+id/udfps_animation_view_legacy" android:layout_width="match_parent" android:layout_height="match_parent"> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index dc9ba8719717..bfe470ca7feb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -85,6 +85,8 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; +import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter; +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -153,6 +155,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final SystemUIDialogManager mDialogManager; @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + @NonNull private final Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels; @NonNull private final VibratorHelper mVibrator; @NonNull private final FeatureFlags mFeatureFlags; @NonNull private final FalsingManager mFalsingManager; @@ -272,7 +275,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { (view, event, fromUdfpsView) -> onTouch(requestId, event, fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags, mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils, - mUdfpsKeyguardAccessibilityDelegate))); + mUdfpsKeyguardAccessibilityDelegate, + mUdfpsKeyguardViewModels))); } @Override @@ -784,7 +788,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { private boolean shouldTryToDismissKeyguard() { return mOverlay != null && mOverlay.getAnimationViewController() - instanceof UdfpsKeyguardViewControllerLegacy + instanceof UdfpsKeyguardViewControllerAdapter && mKeyguardStateController.canDismissLockScreen() && !mAttemptedToDismissKeyguard; } @@ -829,7 +833,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull InputManager inputManager, @NonNull UdfpsUtils udfpsUtils, @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, - @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate) { + @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate, + @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -895,6 +900,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { return Unit.INSTANCE; }); mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mUdfpsKeyguardViewModels = udfpsKeyguardViewModelsProvider; final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController(); mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index e5421471931f..d6ef94d18e71 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -46,15 +46,19 @@ import android.view.accessibility.AccessibilityManager.TouchExplorationStateChan import androidx.annotation.LayoutRes import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor -import com.android.settingslib.udfps.UdfpsUtils import com.android.settingslib.udfps.UdfpsOverlayParams +import com.android.settingslib.udfps.UdfpsUtils import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.flags.Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS +import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -64,6 +68,8 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.settings.SecureSettings +import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Provider private const val TAG = "UdfpsControllerOverlay" @@ -75,6 +81,7 @@ const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" * request. This state can persist across configuration changes via the [show] and [hide] * methods. */ +@ExperimentalCoroutinesApi @UiThread class UdfpsControllerOverlay @JvmOverloads constructor( private val context: Context, @@ -105,6 +112,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, private val udfpsUtils: UdfpsUtils, private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate, + private val udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>, ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -243,27 +251,40 @@ class UdfpsControllerOverlay @JvmOverloads constructor( ) } REASON_AUTH_KEYGUARD -> { - UdfpsKeyguardViewControllerLegacy( - view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) { - updateSensorLocation(sensorBounds) - }, - statusBarStateController, - shadeExpansionStateManager, - statusBarKeyguardViewManager, - keyguardUpdateMonitor, - dumpManager, - transitionController, - configurationController, - keyguardStateController, - unlockedScreenOffAnimationController, - dialogManager, - controller, - activityLaunchAnimator, - featureFlags, - primaryBouncerInteractor, - alternateBouncerInteractor, - udfpsKeyguardAccessibilityDelegate, - ) + if (featureFlags.isEnabled(REFACTOR_UDFPS_KEYGUARD_VIEWS)) { + udfpsKeyguardViewModels.get().setSensorBounds(sensorBounds) + UdfpsKeyguardViewController( + view.addUdfpsView(R.layout.udfps_keyguard_view), + statusBarStateController, + shadeExpansionStateManager, + dialogManager, + dumpManager, + alternateBouncerInteractor, + udfpsKeyguardViewModels.get(), + ) + } else { + UdfpsKeyguardViewControllerLegacy( + view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) { + updateSensorLocation(sensorBounds) + }, + statusBarStateController, + shadeExpansionStateManager, + statusBarKeyguardViewManager, + keyguardUpdateMonitor, + dumpManager, + transitionController, + configurationController, + keyguardStateController, + unlockedScreenOffAnimationController, + dialogManager, + controller, + activityLaunchAnimator, + featureFlags, + primaryBouncerInteractor, + alternateBouncerInteractor, + udfpsKeyguardAccessibilityDelegate, + ) + } } REASON_AUTH_BP -> { // note: empty controller, currently shows no visual affordance @@ -415,7 +436,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( } private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean { - if (animation !is UdfpsKeyguardViewControllerLegacy) { + if (animation !is UdfpsKeyguardViewControllerAdapter) { // always rotate view if we're not on the keyguard return true } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.kt new file mode 100644 index 000000000000..8cc15dadffd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.kt @@ -0,0 +1,58 @@ +/* + * 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.biometrics + +import android.content.Context +import android.util.AttributeSet +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** View corresponding with udfps_keyguard_view.xml */ +@ExperimentalCoroutinesApi +class UdfpsKeyguardView( + context: Context, + attrs: AttributeSet?, +) : + UdfpsAnimationView( + context, + attrs, + ) { + private val fingerprintDrawablePlaceHolder = UdfpsFpDrawable(context) + private var visible = false + + override fun calculateAlpha(): Int { + return if (mPauseAuth) { + 0 + } else 255 // ViewModels handle animating alpha values + } + + override fun getDrawable(): UdfpsDrawable { + return fingerprintDrawablePlaceHolder + } + + fun useExpandedOverlay(useExpandedOverlay: Boolean) { + mUseExpandedOverlay = useExpandedOverlay + } + + fun isVisible(): Boolean { + return visible + } + + fun setVisible(isVisible: Boolean) { + visible = isVisible + isPauseAuth = !visible + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index 9bafeeca24a8..15bd73193687 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -34,6 +34,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionListener @@ -80,7 +81,8 @@ constructor( shadeExpansionStateManager, systemUIDialogManager, dumpManager, - ) { + ), + UdfpsKeyguardViewControllerAdapter { private val useExpandedOverlay: Boolean = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) private var showingUdfpsBouncer = false diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt new file mode 100644 index 000000000000..2a9f3eafc776 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt @@ -0,0 +1,77 @@ +/* + * 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.biometrics.ui.controller + +import com.android.systemui.biometrics.UdfpsAnimationViewController +import com.android.systemui.biometrics.UdfpsKeyguardView +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** Class that coordinates non-HBM animations during keyguard authentication. */ +@ExperimentalCoroutinesApi +open class UdfpsKeyguardViewController( + val view: UdfpsKeyguardView, + statusBarStateController: StatusBarStateController, + shadeExpansionStateManager: ShadeExpansionStateManager, + systemUIDialogManager: SystemUIDialogManager, + dumpManager: DumpManager, + private val alternateBouncerInteractor: AlternateBouncerInteractor, + udfpsKeyguardViewModels: UdfpsKeyguardViewModels, +) : + UdfpsAnimationViewController<UdfpsKeyguardView>( + view, + statusBarStateController, + shadeExpansionStateManager, + systemUIDialogManager, + dumpManager, + ), + UdfpsKeyguardViewControllerAdapter { + override val tag: String + get() = TAG + + init { + udfpsKeyguardViewModels.bindViews(view) + } + + public override fun onViewAttached() { + super.onViewAttached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(true) + } + + public override fun onViewDetached() { + super.onViewDetached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(false) + } + + override fun shouldPauseAuth(): Boolean { + return !view.isVisible() + } + + override fun listenForTouchesOutsideView(): Boolean { + return true + } + + companion object { + private const val TAG = "UdfpsKeyguardViewController" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index a8147d0c2cf6..ff0db34ca06d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -156,7 +156,12 @@ constructor( override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { return ValueAnimator().apply { - interpolator = Interpolators.LINEAR + interpolator = + when (toState) { + KeyguardState.ALTERNATE_BOUNCER -> Interpolators.FAST_OUT_SLOW_IN + else -> Interpolators.LINEAR + } + duration = when (toState) { KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 0cd97b7b025b..45bf20d3ec44 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -61,6 +62,26 @@ constructor( val fromDreamingTransition: Flow<TransitionStep> = repository.transitions.filter { step -> step.from == DREAMING } + /** (any)->Lockscreen transition information */ + val anyStateToLockscreenTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == LOCKSCREEN } + + /** (any)->Occluded transition information */ + val anyStateToOccludedTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == OCCLUDED } + + /** (any)->PrimaryBouncer transition information */ + val anyStateToPrimaryBouncerTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == PRIMARY_BOUNCER } + + /** (any)->Dreaming transition information */ + val anyStateToDreamingTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == DREAMING } + + /** (any)->AlternateBouncer transition information */ + val anyStateToAlternateBouncerTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == ALTERNATE_BOUNCER } + /** AOD->LOCKSCREEN transition information. */ val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) @@ -68,6 +89,9 @@ constructor( val dreamingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DREAMING, LOCKSCREEN) + /** GONE->AOD transition information. */ + val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD) + /** GONE->DREAMING transition information. */ val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt new file mode 100644 index 000000000000..bba0e37d8ed0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt @@ -0,0 +1,65 @@ +/* + * 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.domain.interactor + +import android.animation.FloatEvaluator +import android.animation.IntEvaluator +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** Encapsulates business logic for transitions between UDFPS states on the keyguard. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class UdfpsKeyguardInteractor +@Inject +constructor( + configRepo: ConfigurationRepository, + burnInInteractor: BurnInInteractor, + keyguardInteractor: KeyguardInteractor, +) { + private val intEvaluator = IntEvaluator() + private val floatEvaluator = FloatEvaluator() + + val dozeAmount = keyguardInteractor.dozeAmount + val scaleForResolution = configRepo.scaleForResolution + + /** Burn-in offsets for the UDFPS view to mitigate burn-in on AOD. */ + val burnInOffsets: Flow<BurnInOffsets> = + combine( + keyguardInteractor.dozeAmount, + burnInInteractor.udfpsBurnInXOffset, + burnInInteractor.udfpsBurnInYOffset, + burnInInteractor.udfpsBurnInProgress + ) { dozeAmount, fullyDozingBurnInX, fullyDozingBurnInY, fullyDozingBurnInProgress -> + BurnInOffsets( + intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInX), + intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInY), + floatEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInProgress), + ) + } +} + +data class BurnInOffsets( + val burnInXOffset: Int, // current x burn in offset based on the aodTransitionAmount + val burnInYOffset: Int, // current y burn in offset based on the aodTransitionAmount + val burnInProgress: Float, // current progress based on the aodTransitionAmount +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt new file mode 100644 index 000000000000..ebf1beb132f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt @@ -0,0 +1,25 @@ +/* + * 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.ui.adapter + +/** + * Temporary adapter class while + * [com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController] is being refactored + * before [com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy] is removed. + * + * TODO (b/278719514): Delete once udfps keyguard view is fully refactored. + */ +interface UdfpsKeyguardViewControllerAdapter diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt new file mode 100644 index 000000000000..728dd3911663 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt @@ -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.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch + +@ExperimentalCoroutinesApi +object UdfpsAodFingerprintViewBinder { + + /** + * Drives UI for the UDFPS aod fingerprint view. See [UdfpsFingerprintViewBinder] and + * [UdfpsBackgroundViewBinder]. + */ + @JvmStatic + fun bind( + view: LottieAnimationView, + viewModel: UdfpsAodViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.burnInOffsets.collect { burnInOffsets -> + view.progress = burnInOffsets.burnInProgress + view.translationX = burnInOffsets.burnInXOffset.toFloat() + view.translationY = burnInOffsets.burnInYOffset.toFloat() + } + } + + launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } } + + launch { + viewModel.padding.collect { padding -> + view.setPadding(padding, padding, padding, padding) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt new file mode 100644 index 000000000000..26ef4685d286 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt @@ -0,0 +1,54 @@ +/* + * 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.ui.binder + +import android.content.res.ColorStateList +import android.widget.ImageView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch + +@ExperimentalCoroutinesApi +object UdfpsBackgroundViewBinder { + + /** + * Drives UI for the udfps background view. See [UdfpsAodFingerprintViewBinder] and + * [UdfpsFingerprintViewBinder]. + */ + @JvmStatic + fun bind( + view: ImageView, + viewModel: BackgroundViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.transition.collect { + view.alpha = it.alpha + view.scaleX = it.scale + view.scaleY = it.scale + view.imageTintList = ColorStateList.valueOf(it.color) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt new file mode 100644 index 000000000000..0ab8e52fb6c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt @@ -0,0 +1,86 @@ +/* + * 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.ui.binder + +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.model.KeyPath +import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch + +@ExperimentalCoroutinesApi +object UdfpsFingerprintViewBinder { + private var udfpsIconColor = 0 + + /** + * Drives UI for the UDFPS fingerprint view when it's NOT on aod. See + * [UdfpsAodFingerprintViewBinder] and [UdfpsBackgroundViewBinder]. + */ + @JvmStatic + fun bind( + view: LottieAnimationView, + viewModel: FingerprintViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.transition.collect { + view.alpha = it.alpha + view.scaleX = it.scale + view.scaleY = it.scale + if (udfpsIconColor != (it.color)) { + udfpsIconColor = it.color + view.invalidate() + } + } + } + + launch { + viewModel.burnInOffsets.collect { burnInOffsets -> + view.translationX = burnInOffsets.burnInXOffset.toFloat() + view.translationY = burnInOffsets.burnInYOffset.toFloat() + } + } + + launch { + viewModel.dozeAmount.collect { dozeAmount -> + // Lottie progress represents: aod=0 to lockscreen=1 + view.progress = 1f - dozeAmount + } + } + + launch { + viewModel.padding.collect { padding -> + view.setPadding(padding, padding, padding, padding) + } + } + } + } + + // Add a callback that updates the color to `udfpsIconColor` whenever invalidate is called + view.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(udfpsIconColor, PorterDuff.Mode.SRC_ATOP) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt new file mode 100644 index 000000000000..b568a9af9bb8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt @@ -0,0 +1,55 @@ +/* + * 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.biometrics.ui.binder + +import android.view.View +import com.android.systemui.R +import com.android.systemui.keyguard.ui.binder.UdfpsAodFingerprintViewBinder +import com.android.systemui.keyguard.ui.binder.UdfpsBackgroundViewBinder +import com.android.systemui.keyguard.ui.binder.UdfpsFingerprintViewBinder +import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel +import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel +import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +object UdfpsKeyguardInternalViewBinder { + + @JvmStatic + fun bind( + view: View, + viewModel: UdfpsKeyguardInternalViewModel, + aodViewModel: UdfpsAodViewModel, + fingerprintViewModel: FingerprintViewModel, + backgroundViewModel: BackgroundViewModel, + ) { + view.accessibilityDelegate = viewModel.accessibilityDelegate + + // bind child views + UdfpsAodFingerprintViewBinder.bind(view.findViewById(R.id.udfps_aod_fp), aodViewModel) + UdfpsFingerprintViewBinder.bind( + view.findViewById(R.id.udfps_lockscreen_fp), + fingerprintViewModel + ) + UdfpsBackgroundViewBinder.bind( + view.findViewById(R.id.udfps_keyguard_fp_bg), + backgroundViewModel + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt new file mode 100644 index 000000000000..667abaea0b24 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt @@ -0,0 +1,117 @@ +/* + * 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.ui.binder + +import android.graphics.RectF +import android.view.View +import android.widget.FrameLayout +import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.biometrics.UdfpsKeyguardView +import com.android.systemui.biometrics.ui.binder.UdfpsKeyguardInternalViewBinder +import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel +import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel +import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@ExperimentalCoroutinesApi +object UdfpsKeyguardViewBinder { + /** + * Drives UI for the keyguard UDFPS view. Inflates child views on a background thread. For view + * binders for its child views, see [UdfpsFingerprintViewBinder], [UdfpsBackgroundViewBinder] & + * [UdfpsAodFingerprintViewBinder]. + */ + @JvmStatic + fun bind( + view: UdfpsKeyguardView, + viewModel: UdfpsKeyguardViewModel, + udfpsKeyguardInternalViewModel: UdfpsKeyguardInternalViewModel, + aodViewModel: UdfpsAodViewModel, + fingerprintViewModel: FingerprintViewModel, + backgroundViewModel: BackgroundViewModel, + ) { + view.useExpandedOverlay(viewModel.useExpandedOverlay()) + + val layoutInflaterFinishListener = + AsyncLayoutInflater.OnInflateFinishedListener { inflatedInternalView, _, parent -> + UdfpsKeyguardInternalViewBinder.bind( + inflatedInternalView, + udfpsKeyguardInternalViewModel, + aodViewModel, + fingerprintViewModel, + backgroundViewModel, + ) + if (viewModel.useExpandedOverlay()) { + val lp = inflatedInternalView.layoutParams as FrameLayout.LayoutParams + lp.width = viewModel.sensorBounds.width() + lp.height = viewModel.sensorBounds.height() + val relativeToView = + getBoundsRelativeToView( + inflatedInternalView, + RectF(viewModel.sensorBounds), + ) + lp.setMarginsRelative( + relativeToView.left.toInt(), + relativeToView.top.toInt(), + relativeToView.right.toInt(), + relativeToView.bottom.toInt(), + ) + parent!!.addView(inflatedInternalView, lp) + } else { + parent!!.addView(inflatedInternalView) + } + } + val inflater = AsyncLayoutInflater(view.context) + inflater.inflate(R.layout.udfps_keyguard_view_internal, view, layoutInflaterFinishListener) + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + combine(aodViewModel.isVisible, fingerprintViewModel.visible) { + isAodVisible, + isFingerprintVisible -> + isAodVisible || isFingerprintVisible + } + .collect { view.setVisible(it) } + } + } + } + } + + /** + * Converts coordinates of RectF relative to the screen to coordinates relative to this view. + * + * @param bounds RectF based off screen coordinates in current orientation + */ + private fun getBoundsRelativeToView(view: View, bounds: RectF): RectF { + val pos: IntArray = view.locationOnScreen + return RectF( + bounds.left - pos[0], + bounds.top - pos[1], + bounds.right - pos[0], + bounds.bottom - pos[1] + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt new file mode 100644 index 000000000000..667c2f1bd998 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt @@ -0,0 +1,47 @@ +/* + * 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.ui.viewmodel + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.keyguard.domain.interactor.BurnInOffsets +import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** View-model for UDFPS AOD view. */ +@ExperimentalCoroutinesApi +class UdfpsAodViewModel +@Inject +constructor( + val interactor: UdfpsKeyguardInteractor, + val context: Context, +) { + val alpha: Flow<Float> = interactor.dozeAmount + val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets + val isVisible: Flow<Boolean> = alpha.map { it != 0f } + + // Padding between the fingerprint icon and its bounding box in pixels. + val padding: Flow<Int> = + interactor.scaleForResolution.map { scale -> + (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) + .roundToInt() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt new file mode 100644 index 000000000000..d894a1139eeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt @@ -0,0 +1,26 @@ +/* + * 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.ui.viewmodel + +import com.android.systemui.biometrics.UdfpsKeyguardAccessibilityDelegate +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +class UdfpsKeyguardInternalViewModel +@Inject +constructor(val accessibilityDelegate: UdfpsKeyguardAccessibilityDelegate) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt new file mode 100644 index 000000000000..929f27f4fea3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt @@ -0,0 +1,36 @@ +/* + * 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.ui.viewmodel + +import android.graphics.Rect +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +class UdfpsKeyguardViewModel +@Inject +constructor( + private val featureFlags: FeatureFlags, +) { + var sensorBounds: Rect = Rect() + + fun useExpandedOverlay(): Boolean { + return featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt new file mode 100644 index 000000000000..098b481491de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt @@ -0,0 +1,36 @@ +package com.android.systemui.keyguard.ui.viewmodel + +import android.graphics.Rect +import com.android.systemui.biometrics.UdfpsKeyguardView +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.ui.binder.UdfpsKeyguardViewBinder +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +@SysUISingleton +class UdfpsKeyguardViewModels +@Inject +constructor( + private val viewModel: UdfpsKeyguardViewModel, + private val internalViewModel: UdfpsKeyguardInternalViewModel, + private val aodViewModel: UdfpsAodViewModel, + private val lockscreenFingerprintViewModel: FingerprintViewModel, + private val lockscreenBackgroundViewModel: BackgroundViewModel, +) { + + fun setSensorBounds(sensorBounds: Rect) { + viewModel.sensorBounds = sensorBounds + } + + fun bindViews(view: UdfpsKeyguardView) { + UdfpsKeyguardViewBinder.bind( + view, + viewModel, + internalViewModel, + aodViewModel, + lockscreenFingerprintViewModel, + lockscreenBackgroundViewModel + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt new file mode 100644 index 000000000000..fd4b666a80fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt @@ -0,0 +1,162 @@ +/* + * 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.ui.viewmodel + +import android.content.Context +import androidx.annotation.ColorInt +import com.android.settingslib.Utils.getColorAttrDefaultColor +import com.android.systemui.R +import com.android.systemui.keyguard.domain.interactor.BurnInOffsets +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** View-model for UDFPS lockscreen views. */ +@ExperimentalCoroutinesApi +open class UdfpsLockscreenViewModel( + context: Context, + lockscreenColorResId: Int, + alternateBouncerColorResId: Int, + transitionInteractor: KeyguardTransitionInteractor, +) { + private val toLockscreen: Flow<TransitionViewModel> = + transitionInteractor.anyStateToLockscreenTransition.map { + TransitionViewModel( + alpha = + if (it.from == KeyguardState.AOD) { + it.value // animate + } else { + 1f + }, + scale = 1f, + color = getColorAttrDefaultColor(context, lockscreenColorResId), + ) + } + + private val toAlternateBouncer: Flow<TransitionViewModel> = + transitionInteractor.anyStateToAlternateBouncerTransition.map { + TransitionViewModel( + alpha = 1f, + scale = + if (visibleInKeyguardState(it.from)) { + 1f + } else { + it.value + }, + color = getColorAttrDefaultColor(context, alternateBouncerColorResId), + ) + } + + private val fadeOut: Flow<TransitionViewModel> = + merge( + transitionInteractor.anyStateToGoneTransition, + transitionInteractor.anyStateToAodTransition, + transitionInteractor.anyStateToOccludedTransition, + transitionInteractor.anyStateToPrimaryBouncerTransition, + transitionInteractor.anyStateToDreamingTransition, + ) + .map { + TransitionViewModel( + alpha = + if (visibleInKeyguardState(it.from)) { + 1f - it.value + } else { + 0f + }, + scale = 1f, + color = + if (it.from == KeyguardState.ALTERNATE_BOUNCER) { + getColorAttrDefaultColor(context, alternateBouncerColorResId) + } else { + getColorAttrDefaultColor(context, lockscreenColorResId) + }, + ) + } + + private fun visibleInKeyguardState(state: KeyguardState): Boolean { + return when (state) { + KeyguardState.OFF, + KeyguardState.DOZING, + KeyguardState.DREAMING, + KeyguardState.AOD, + KeyguardState.PRIMARY_BOUNCER, + KeyguardState.GONE, + KeyguardState.OCCLUDED -> false + KeyguardState.LOCKSCREEN, + KeyguardState.ALTERNATE_BOUNCER -> true + } + } + + val transition: Flow<TransitionViewModel> = + merge( + toAlternateBouncer, + toLockscreen, + fadeOut, + ) + val visible: Flow<Boolean> = transition.map { it.alpha != 0f } +} + +@ExperimentalCoroutinesApi +class FingerprintViewModel +@Inject +constructor( + val context: Context, + transitionInteractor: KeyguardTransitionInteractor, + interactor: UdfpsKeyguardInteractor, +) : + UdfpsLockscreenViewModel( + context, + android.R.attr.textColorPrimary, + com.android.internal.R.attr.materialColorOnPrimaryFixed, + transitionInteractor, + ) { + val dozeAmount: Flow<Float> = interactor.dozeAmount + val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets + + // Padding between the fingerprint icon and its bounding box in pixels. + val padding: Flow<Int> = + interactor.scaleForResolution.map { scale -> + (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) + .roundToInt() + } +} + +@ExperimentalCoroutinesApi +class BackgroundViewModel +@Inject +constructor( + val context: Context, + transitionInteractor: KeyguardTransitionInteractor, +) : + UdfpsLockscreenViewModel( + context, + com.android.internal.R.attr.colorSurface, + com.android.internal.R.attr.materialColorPrimaryFixed, + transitionInteractor, + ) + +data class TransitionViewModel( + val alpha: Float, + val scale: Float, + @ColorInt val color: Int, +) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 224875590d75..0e0d0e3f3748 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -43,11 +43,12 @@ import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -58,6 +59,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Rule import org.junit.Test @@ -70,6 +72,7 @@ import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import javax.inject.Provider import org.mockito.Mockito.`when` as whenever private const val REQUEST_ID = 2L @@ -80,6 +83,7 @@ private const val DISPLAY_HEIGHT = 1920 private const val SENSOR_WIDTH = 30 private const val SENSOR_HEIGHT = 60 +@ExperimentalCoroutinesApi @SmallTest @RoboPilotTest @RunWith(AndroidJUnit4::class) @@ -116,6 +120,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var udfpsUtils: UdfpsUtils @Mock private lateinit var udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate + @Mock private lateinit var udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels> @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } @@ -148,6 +153,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { controllerCallback, onTouch, activityLaunchAnimator, featureFlags, primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils, udfpsKeyguardAccessibilityDelegate, + udfpsKeyguardViewModels, ) block() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 30e54474bbde..eb9ce1d9453a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -83,13 +83,14 @@ import com.android.systemui.biometrics.udfps.InteractionEvent; import com.android.systemui.biometrics.udfps.NormalizedTouchData; import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor; import com.android.systemui.biometrics.udfps.TouchProcessorResult; +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; +import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -219,6 +220,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private SecureSettings mSecureSettings; @Mock private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; + @Mock + private Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels; // Capture listeners so that they can be used to send events @Captor @@ -318,7 +321,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker, mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils, mock(KeyguardFaceAuthInteractor.class), - mUdfpsKeyguardAccessibilityDelegate); + mUdfpsKeyguardAccessibilityDelegate, mUdfpsKeyguardViewModels); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt new file mode 100644 index 000000000000..1baca2184e9b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt @@ -0,0 +1,168 @@ +/* + * 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.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +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.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class UdfpsKeyguardInteractorTest : SysuiTestCase() { + private val burnInProgress = 1f + private val burnInYOffset = 20 + private val burnInXOffset = 10 + + private lateinit var testScope: TestScope + private lateinit var configRepository: FakeConfigurationRepository + private lateinit var bouncerRepository: KeyguardBouncerRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var fakeCommandQueue: FakeCommandQueue + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var burnInInteractor: BurnInInteractor + + @Mock private lateinit var burnInHelper: BurnInHelperWrapper + + private lateinit var underTest: UdfpsKeyguardInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + testScope = TestScope() + configRepository = FakeConfigurationRepository() + keyguardRepository = FakeKeyguardRepository() + bouncerRepository = FakeKeyguardBouncerRepository() + fakeCommandQueue = FakeCommandQueue() + featureFlags = + FakeFeatureFlags().apply { + set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true) + set(Flags.FACE_AUTH_REFACTOR, false) + } + burnInInteractor = + BurnInInteractor( + context, + burnInHelper, + testScope.backgroundScope, + configRepository, + FakeSystemClock(), + ) + + underTest = + UdfpsKeyguardInteractor( + configRepository, + burnInInteractor, + KeyguardInteractor( + keyguardRepository, + fakeCommandQueue, + featureFlags, + bouncerRepository, + configRepository, + ), + ) + } + + @Test + fun dozeChanges_updatesUdfpsAodModel() = + testScope.runTest { + val burnInOffsets by collectLastValue(underTest.burnInOffsets) + initializeBurnInOffsets() + + // WHEN we're not dozing + setAwake() + runCurrent() + + // THEN burn in offsets are 0 + assertThat(burnInOffsets?.burnInProgress).isEqualTo(0f) + assertThat(burnInOffsets?.burnInYOffset).isEqualTo(0) + assertThat(burnInOffsets?.burnInXOffset).isEqualTo(0) + + // WHEN we're in the middle of the doze amount change + keyguardRepository.setDozeAmount(.50f) + runCurrent() + + // THEN burn in is updated (between 0 and the full offset) + assertThat(burnInOffsets?.burnInProgress).isGreaterThan(0f) + assertThat(burnInOffsets?.burnInYOffset).isGreaterThan(0) + assertThat(burnInOffsets?.burnInXOffset).isGreaterThan(0) + assertThat(burnInOffsets?.burnInProgress).isLessThan(burnInProgress) + assertThat(burnInOffsets?.burnInYOffset).isLessThan(burnInYOffset) + assertThat(burnInOffsets?.burnInXOffset).isLessThan(burnInXOffset) + + // WHEN we're fully dozing + keyguardRepository.setDozeAmount(1f) + runCurrent() + + // THEN burn in offsets are updated to final current values (for the given time) + assertThat(burnInOffsets?.burnInProgress).isEqualTo(burnInProgress) + assertThat(burnInOffsets?.burnInYOffset).isEqualTo(burnInYOffset) + assertThat(burnInOffsets?.burnInXOffset).isEqualTo(burnInXOffset) + } + + private fun initializeBurnInOffsets() { + whenever(burnInHelper.burnInProgressOffset()).thenReturn(burnInProgress) + whenever(burnInHelper.burnInOffset(anyInt(), /* xAxis */ eq(true))) + .thenReturn(burnInXOffset) + whenever(burnInHelper.burnInOffset(anyInt(), /* xAxis */ eq(false))) + .thenReturn(burnInYOffset) + } + + private fun setAwake() { + keyguardRepository.setDozeAmount(0f) + burnInInteractor.dozeTimeTick() + + bouncerRepository.setAlternateVisible(false) + keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + bouncerRepository.setPrimaryShow(false) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + ) + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt new file mode 100644 index 000000000000..436c09ca4a05 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt @@ -0,0 +1,149 @@ +/* + * 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.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor +import com.android.systemui.util.time.FakeSystemClock +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.Mock +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +class UdfpsAodViewModelTest : SysuiTestCase() { + private val defaultPadding = 12 + private lateinit var underTest: UdfpsAodViewModel + + private lateinit var testScope: TestScope + private lateinit var configRepository: FakeConfigurationRepository + private lateinit var bouncerRepository: KeyguardBouncerRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var fakeCommandQueue: FakeCommandQueue + private lateinit var featureFlags: FakeFeatureFlags + + @Mock private lateinit var burnInHelper: BurnInHelperWrapper + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + overrideResource(com.android.systemui.R.dimen.lock_icon_padding, defaultPadding) + testScope = TestScope() + configRepository = FakeConfigurationRepository() + keyguardRepository = FakeKeyguardRepository() + bouncerRepository = FakeKeyguardBouncerRepository() + fakeCommandQueue = FakeCommandQueue() + featureFlags = + FakeFeatureFlags().apply { + set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true) + set(Flags.FACE_AUTH_REFACTOR, false) + } + + val udfpsKeyguardInteractor = + UdfpsKeyguardInteractor( + configRepository, + BurnInInteractor( + context, + burnInHelper, + testScope.backgroundScope, + configRepository, + FakeSystemClock(), + ), + KeyguardInteractor( + keyguardRepository, + fakeCommandQueue, + featureFlags, + bouncerRepository, + configRepository, + ), + ) + + underTest = + UdfpsAodViewModel( + udfpsKeyguardInteractor, + context, + ) + } + + @Test + fun alphaAndVisibleUpdates_onDozeAmountChanges() = + testScope.runTest { + val alpha by collectLastValue(underTest.alpha) + val visible by collectLastValue(underTest.isVisible) + + keyguardRepository.setDozeAmount(0f) + runCurrent() + assertThat(alpha).isEqualTo(0f) + assertThat(visible).isFalse() + + keyguardRepository.setDozeAmount(.65f) + runCurrent() + assertThat(alpha).isEqualTo(.65f) + assertThat(visible).isTrue() + + keyguardRepository.setDozeAmount(.23f) + runCurrent() + assertThat(alpha).isEqualTo(.23f) + assertThat(visible).isTrue() + + keyguardRepository.setDozeAmount(1f) + runCurrent() + assertThat(alpha).isEqualTo(1f) + assertThat(visible).isTrue() + } + + @Test + fun paddingUpdates_onScaleForResolutionChanges() = + testScope.runTest { + val padding by collectLastValue(underTest.padding) + + configRepository.setScaleForResolution(1f) + runCurrent() + assertThat(padding).isEqualTo(defaultPadding) + + configRepository.setScaleForResolution(2f) + runCurrent() + assertThat(padding).isEqualTo(defaultPadding * 2) + + configRepository.setScaleForResolution(.5f) + runCurrent() + assertThat(padding).isEqualTo((defaultPadding * .5f).toInt()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt new file mode 100644 index 000000000000..a30e2a601e9d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt @@ -0,0 +1,131 @@ +/* + * 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.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor +import com.android.systemui.util.time.FakeSystemClock +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.Mock +import org.mockito.MockitoAnnotations + +/** Tests UdfpsFingerprintViewModel specific flows. */ +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class UdfpsFingerprintViewModelTest : SysuiTestCase() { + private val defaultPadding = 12 + private lateinit var underTest: FingerprintViewModel + + private lateinit var testScope: TestScope + private lateinit var configRepository: FakeConfigurationRepository + private lateinit var bouncerRepository: KeyguardBouncerRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var fakeCommandQueue: FakeCommandQueue + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var transitionRepository: FakeKeyguardTransitionRepository + + @Mock private lateinit var burnInHelper: BurnInHelperWrapper + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + overrideResource(com.android.systemui.R.dimen.lock_icon_padding, defaultPadding) + testScope = TestScope() + configRepository = FakeConfigurationRepository() + keyguardRepository = FakeKeyguardRepository() + bouncerRepository = FakeKeyguardBouncerRepository() + fakeCommandQueue = FakeCommandQueue() + featureFlags = + FakeFeatureFlags().apply { + set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true) + set(Flags.FACE_AUTH_REFACTOR, false) + } + bouncerRepository = FakeKeyguardBouncerRepository() + transitionRepository = FakeKeyguardTransitionRepository() + val transitionInteractor = + KeyguardTransitionInteractor( + transitionRepository, + testScope.backgroundScope, + ) + val udfpsKeyguardInteractor = + UdfpsKeyguardInteractor( + configRepository, + BurnInInteractor( + context, + burnInHelper, + testScope.backgroundScope, + configRepository, + FakeSystemClock(), + ), + KeyguardInteractor( + keyguardRepository, + fakeCommandQueue, + featureFlags, + bouncerRepository, + configRepository, + ), + ) + + underTest = + FingerprintViewModel( + context, + transitionInteractor, + udfpsKeyguardInteractor, + ) + } + + @Test + fun paddingUpdates_onScaleForResolutionChanges() = + testScope.runTest { + val padding by collectLastValue(underTest.padding) + + configRepository.setScaleForResolution(1f) + runCurrent() + assertThat(padding).isEqualTo(defaultPadding) + + configRepository.setScaleForResolution(2f) + runCurrent() + assertThat(padding).isEqualTo(defaultPadding * 2) + + configRepository.setScaleForResolution(.5f) + runCurrent() + assertThat(padding).isEqualTo((defaultPadding * .5).toInt()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt new file mode 100644 index 000000000000..d58ceee40c68 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt @@ -0,0 +1,505 @@ +/* + * 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.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.Utils +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.collect.Range +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.MockitoAnnotations + +/** Tests UDFPS lockscreen view model transitions. */ +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class UdfpsLockscreenViewModelTest : SysuiTestCase() { + private val lockscreenColorResId = android.R.attr.textColorPrimary + private val alternateBouncerResId = com.android.internal.R.attr.materialColorOnPrimaryFixed + private val lockscreenColor = Utils.getColorAttrDefaultColor(context, lockscreenColorResId) + private val alternateBouncerColor = + Utils.getColorAttrDefaultColor(context, alternateBouncerResId) + + private lateinit var underTest: UdfpsLockscreenViewModel + private lateinit var testScope: TestScope + private lateinit var transitionRepository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope() + transitionRepository = FakeKeyguardTransitionRepository() + val transitionInteractor = + KeyguardTransitionInteractor( + transitionRepository, + testScope.backgroundScope, + ) + underTest = + UdfpsLockscreenViewModel( + context, + lockscreenColorResId, + alternateBouncerResId, + transitionInteractor, + ) + } + + @Test + fun goneToAodTransition() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: gone -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "goneToAodTransition", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(visible).isFalse() + + // TransitionState.RUNNING: gone -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "goneToAodTransition", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(visible).isFalse() + + // TransitionState.FINISHED: gone -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "goneToAodTransition", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(visible).isFalse() + } + + @Test + fun lockscreenToAod() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: lockscreen -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "lockscreenToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: lockscreen -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "lockscreenToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: lockscreen -> AOD + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "lockscreenToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isFalse() + } + + @Test + fun aodToLockscreen() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: AOD -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "aodToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isFalse() + + // TransitionState.RUNNING: AOD -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "aodToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f)) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: AOD -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "aodToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + } + + @Test + fun lockscreenToAlternateBouncer() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: lockscreen -> alternate bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.ALTERNATE_BOUNCER, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "lockscreenToAlternateBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: lockscreen -> alternate bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.ALTERNATE_BOUNCER, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "lockscreenToAlternateBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: lockscreen -> alternate bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.ALTERNATE_BOUNCER, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "lockscreenToAlternateBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + } + + fun alternateBouncerToPrimaryBouncer() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: alternate bouncer -> primary bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.PRIMARY_BOUNCER, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "alternateBouncerToPrimaryBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: alternate bouncer -> primary bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.PRIMARY_BOUNCER, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "alternateBouncerToPrimaryBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f)) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: alternate bouncer -> primary bouncer + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.PRIMARY_BOUNCER, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "alternateBouncerToPrimaryBouncer", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isFalse() + } + + fun alternateBouncerToAod() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: alternate bouncer -> aod + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.AOD, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "alternateBouncerToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: alternate bouncer -> aod + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.AOD, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "alternateBouncerToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: alternate bouncer -> aod + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "alternateBouncerToAod", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(alternateBouncerColor) + assertThat(visible).isFalse() + } + + @Test + fun lockscreenToOccluded() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: lockscreen -> occluded + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "lockscreenToOccluded", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: lockscreen -> occluded + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "lockscreenToOccluded", + ) + ) + runCurrent() + assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: lockscreen -> occluded + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "lockscreenToOccluded", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(0f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isFalse() + } + + @Test + fun occludedToLockscreen() = + testScope.runTest { + val transition by collectLastValue(underTest.transition) + val visible by collectLastValue(underTest.visible) + + // TransitionState.STARTED: occluded -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + value = 0f, + transitionState = TransitionState.STARTED, + ownerName = "occludedToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.RUNNING: occluded -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + value = .6f, + transitionState = TransitionState.RUNNING, + ownerName = "occludedToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + + // TransitionState.FINISHED: occluded -> lockscreen + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + value = 1f, + transitionState = TransitionState.FINISHED, + ownerName = "occludedToLockscreen", + ) + ) + runCurrent() + assertThat(transition?.alpha).isEqualTo(1f) + assertThat(transition?.scale).isEqualTo(1f) + assertThat(transition?.color).isEqualTo(lockscreenColor) + assertThat(visible).isTrue() + } +} |