summaryrefslogtreecommitdiff
path: root/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
blob: 110bcd715be22c335c958f39f037dd26e5efcbbe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/*
 * 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.keyguard.domain.interactor

import android.content.Context
import android.content.res.ColorStateList
import android.hardware.biometrics.BiometricSourceType
import android.os.Handler
import android.os.Trace
import android.util.Log
import android.view.View
import com.android.keyguard.KeyguardConstants
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.DejankUtils
import com.android.systemui.R
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.policy.KeyguardStateController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import javax.inject.Inject

/**
 * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password)
 * bouncer.
 */
@SysUISingleton
class PrimaryBouncerInteractor
@Inject
constructor(
        private val repository: KeyguardBouncerRepository,
        private val primaryBouncerView: BouncerView,
        @Main private val mainHandler: Handler,
        private val keyguardStateController: KeyguardStateController,
        private val keyguardSecurityModel: KeyguardSecurityModel,
        private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
        private val falsingCollector: FalsingCollector,
        private val dismissCallbackRegistry: DismissCallbackRegistry,
        private val context: Context,
        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
        private val trustRepository: TrustRepository,
        private val featureFlags: FeatureFlags,
) {
    private val passiveAuthBouncerDelay = context.resources.getInteger(
            R.integer.primary_bouncer_passive_auth_delay).toLong()
    /** Runnable to show the primary bouncer. */
    val showRunnable = Runnable {
        repository.setPrimaryShow(true)
        repository.setPrimaryShowingSoon(false)
        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
    }

    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
    val isShowing: Flow<Boolean> = repository.primaryBouncerShow
    val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
    val startingDisappearAnimation: Flow<Runnable> =
        repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
    val keyguardPosition: Flow<Float> = repository.keyguardPosition
    val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
    /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
    val bouncerExpansion: Flow<Float> =
        combine(repository.panelExpansionAmount, repository.primaryBouncerShow) {
            panelExpansion,
            primaryBouncerIsShowing ->
            if (primaryBouncerIsShowing) {
                1f - panelExpansion
            } else {
                0f
            }
        }
    /** Allow for interaction when just about fully visible */
    val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
    val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing

    /** This callback needs to be a class field so it does not get garbage collected. */
    val keyguardUpdateMonitorCallback =
        object : KeyguardUpdateMonitorCallback() {
            override fun onBiometricRunningStateChanged(
                running: Boolean,
                biometricSourceType: BiometricSourceType?
            ) {
                updateSideFpsVisibility()
            }

            override fun onStrongAuthStateChanged(userId: Int) {
                updateSideFpsVisibility()
            }
        }

    init {
        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
    }

    // TODO(b/243685699): Move isScrimmed logic to data layer.
    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
    /** Show the bouncer if necessary and set the relevant states. */
    @JvmOverloads
    fun show(isScrimmed: Boolean) {
        // Reset some states as we show the bouncer.
        repository.setKeyguardAuthenticated(null)
        repository.setPrimaryStartingToHide(false)

        val resumeBouncer =
            (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
                needsFullscreenBouncer()

        Trace.beginSection("KeyguardBouncer#show")
        repository.setPrimaryScrimmed(isScrimmed)
        if (isScrimmed) {
            setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
        }

        // In this special case, we want to hide the bouncer and show it again. We want to emit
        // show(true) again so that we can reinflate the new view.
        if (resumeBouncer) {
            repository.setPrimaryShow(false)
        }

        if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
            // Keyguard is done.
            return
        }

        repository.setPrimaryShowingSoon(true)
        if (usePrimaryBouncerPassiveAuthDelay()) {
            Log.d(TAG, "delay bouncer, passive auth may succeed")
            mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
        } else {
            DejankUtils.postAfterTraversal(showRunnable)
        }
        keyguardStateController.notifyPrimaryBouncerShowing(true)
        primaryBouncerCallbackInteractor.dispatchStartingToShow()
        Trace.endSection()
    }

    /** Sets the correct bouncer states to hide the bouncer. */
    fun hide() {
        Trace.beginSection("KeyguardBouncer#hide")
        if (isFullyShowing()) {
            SysUiStatsLog.write(
                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
            )
            dismissCallbackRegistry.notifyDismissCancelled()
        }

        repository.setPrimaryStartDisappearAnimation(null)
        falsingCollector.onBouncerHidden()
        keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */)
        cancelShowRunnable()
        repository.setPrimaryShowingSoon(false)
        repository.setPrimaryShow(false)
        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
        Trace.endSection()
    }

    /**
     * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
     * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
     * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
     * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
     * of 0f represents the bouncer fully showing.
     */
    fun setPanelExpansion(expansion: Float) {
        val oldExpansion = repository.panelExpansionAmount.value
        val expansionChanged = oldExpansion != expansion
        if (repository.primaryBouncerStartingDisappearAnimation.value == null) {
            repository.setPanelExpansion(expansion)
        }

        if (
            expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
                oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
        ) {
            falsingCollector.onBouncerShown()
            primaryBouncerCallbackInteractor.dispatchFullyShown()
        } else if (
            expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
                oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
        ) {
            /*
             * There are cases where #hide() was not invoked, such as when
             * NotificationPanelViewController controls the hide animation. Make sure the state gets
             * updated by calling #hide() directly.
             */
            hide()
            DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
            primaryBouncerCallbackInteractor.dispatchFullyHidden()
        } else if (
            expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
                oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
        ) {
            primaryBouncerCallbackInteractor.dispatchStartingToHide()
            repository.setPrimaryStartingToHide(true)
        }
        if (expansionChanged) {
            primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion)
        }
    }

    /** Set the initial keyguard message to show when bouncer is shown. */
    fun showMessage(message: String?, colorStateList: ColorStateList?) {
        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
    }

    /**
     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
     * call cancelAction.
     */
    fun setDismissAction(
        onDismissAction: ActivityStarter.OnDismissAction?,
        cancelAction: Runnable?
    ) {
        primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
    }

    /** Update the resources of the views. */
    fun updateResources() {
        repository.setResourceUpdateRequests(true)
    }

    /** Tell the bouncer that keyguard is authenticated. */
    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
        repository.setKeyguardAuthenticated(strongAuth)
    }

    /** Update the position of the bouncer when showing. */
    fun setKeyguardPosition(position: Float) {
        repository.setKeyguardPosition(position)
    }

    /** Notifies that the state change was handled. */
    fun notifyKeyguardAuthenticatedHandled() {
        repository.setKeyguardAuthenticated(null)
    }

    /** Notifies that the message was shown. */
    fun onMessageShown() {
        repository.setShowMessage(null)
    }

    /** Notify that the resources have been updated */
    fun notifyUpdatedResources() {
        repository.setResourceUpdateRequests(false)
    }

    /** Set whether back button is enabled when on the bouncer screen. */
    fun setBackButtonEnabled(enabled: Boolean) {
        repository.setIsBackButtonEnabled(enabled)
    }

    /** Tell the bouncer to start the pre hide animation. */
    fun startDisappearAnimation(runnable: Runnable) {
        if (willRunDismissFromKeyguard()) {
            runnable.run()
            return
        }

        repository.setPrimaryStartDisappearAnimation(runnable)
    }

    /** Determine whether to show the side fps animation. */
    fun updateSideFpsVisibility() {
        val sfpsEnabled: Boolean =
            context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
        val fpsDetectionRunning: Boolean = keyguardUpdateMonitor.isFingerprintDetectionRunning
        val isUnlockingWithFpAllowed: Boolean =
            keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed
        val toShow =
            (isBouncerShowing() &&
                sfpsEnabled &&
                fpsDetectionRunning &&
                isUnlockingWithFpAllowed &&
                !isAnimatingAway())

        if (KeyguardConstants.DEBUG) {
            Log.d(
                TAG,
                ("sideFpsToShow=$toShow\n" +
                    "isBouncerShowing=${isBouncerShowing()}\n" +
                    "configEnabled=$sfpsEnabled\n" +
                    "fpsDetectionRunning=$fpsDetectionRunning\n" +
                    "isUnlockingWithFpAllowed=$isUnlockingWithFpAllowed\n" +
                    "isAnimatingAway=${isAnimatingAway()}")
            )
        }
        repository.setSideFpsShowing(toShow)
    }

    /** Returns whether bouncer is fully showing. */
    fun isFullyShowing(): Boolean {
        return (repository.primaryBouncerShowingSoon.value || isBouncerShowing()) &&
            repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
            repository.primaryBouncerStartingDisappearAnimation.value == null
    }

    /** Returns whether bouncer is scrimmed. */
    fun isScrimmed(): Boolean {
        return repository.primaryBouncerScrimmed.value
    }

    /** If bouncer expansion is between 0f and 1f non-inclusive. */
    fun isInTransit(): Boolean {
        return repository.primaryBouncerShowingSoon.value ||
            repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
                repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
    }

    /** Return whether bouncer is animating away. */
    fun isAnimatingAway(): Boolean {
        return repository.primaryBouncerStartingDisappearAnimation.value != null
    }

    /** Return whether bouncer will dismiss with actions */
    fun willDismissWithAction(): Boolean {
        return primaryBouncerView.delegate?.willDismissWithActions() == true
    }

    /** Will the dismissal run from the keyguard layout (instead of from bouncer) */
    fun willRunDismissFromKeyguard(): Boolean {
        return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true
    }

    /** Returns whether the bouncer should be full screen. */
    private fun needsFullscreenBouncer(): Boolean {
        val mode: KeyguardSecurityModel.SecurityMode =
            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
            mode == KeyguardSecurityModel.SecurityMode.SimPuk
    }

    /** Remove the show runnable from the main handler queue to improve performance. */
    private fun cancelShowRunnable() {
        DejankUtils.removeCallbacks(showRunnable)
        mainHandler.removeCallbacks(showRunnable)
    }

    private fun isBouncerShowing(): Boolean {
        return repository.primaryBouncerShow.value
    }

    /** Whether we want to wait to show the bouncer in case passive auth succeeds. */
    private fun usePrimaryBouncerPassiveAuthDelay(): Boolean {
        val canRunFaceAuth = keyguardStateController.isFaceAuthEnabled &&
                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)
        val canRunActiveUnlock = trustRepository.isCurrentUserActiveUnlockAvailable.value &&
                keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()
        return featureFlags.isEnabled(Flags.DELAY_BOUNCER) &&
                !needsFullscreenBouncer() &&
                (canRunFaceAuth || canRunActiveUnlock)
    }

    companion object {
        private const val TAG = "PrimaryBouncerInteractor"
    }
}