1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.bouncer.domain.interactor 18 19 import android.os.CountDownTimer 20 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT 21 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT 22 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT 23 import com.android.systemui.bouncer.data.factory.BouncerMessageFactory 24 import com.android.systemui.bouncer.data.repository.BouncerMessageRepository 25 import com.android.systemui.bouncer.shared.model.BouncerMessageModel 26 import com.android.systemui.dagger.SysUISingleton 27 import com.android.systemui.flags.FeatureFlags 28 import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES 29 import com.android.systemui.user.data.repository.UserRepository 30 import javax.inject.Inject 31 import kotlin.math.roundToInt 32 import kotlinx.coroutines.flow.Flow 33 import kotlinx.coroutines.flow.combine 34 import kotlinx.coroutines.flow.distinctUntilChanged 35 import kotlinx.coroutines.flow.flowOf 36 import kotlinx.coroutines.flow.map 37 38 @SysUISingleton 39 class BouncerMessageInteractor 40 @Inject 41 constructor( 42 private val repository: BouncerMessageRepository, 43 private val factory: BouncerMessageFactory, 44 private val userRepository: UserRepository, 45 private val countDownTimerUtil: CountDownTimerUtil, 46 private val featureFlags: FeatureFlags, 47 ) { 48 fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) { 49 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 50 51 val callback = 52 object : CountDownTimerCallback { 53 override fun onFinish() { 54 repository.clearMessage() 55 } 56 57 override fun onTick(millisUntilFinished: Long) { 58 val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt() 59 val message = 60 factory.createFromPromptReason( 61 reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT, 62 userId = userRepository.getSelectedUserInfo().id 63 ) 64 message?.message?.animate = false 65 message?.message?.formatterArgs = 66 mutableMapOf<String, Any>(Pair("count", secondsRemaining)) 67 repository.setPrimaryAuthMessage(message) 68 } 69 } 70 countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback) 71 } 72 73 fun onPrimaryAuthIncorrectAttempt() { 74 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 75 76 repository.setPrimaryAuthMessage( 77 factory.createFromPromptReason( 78 PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, 79 userRepository.getSelectedUserInfo().id 80 ) 81 ) 82 } 83 84 fun setFingerprintAcquisitionMessage(value: String?) { 85 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 86 87 repository.setFingerprintAcquisitionMessage( 88 if (value != null) { 89 factory.createFromPromptReason( 90 PROMPT_REASON_DEFAULT, 91 userRepository.getSelectedUserInfo().id, 92 secondaryMsgOverride = value 93 ) 94 } else { 95 null 96 } 97 ) 98 } 99 100 fun setFaceAcquisitionMessage(value: String?) { 101 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 102 103 repository.setFaceAcquisitionMessage( 104 if (value != null) { 105 factory.createFromPromptReason( 106 PROMPT_REASON_DEFAULT, 107 userRepository.getSelectedUserInfo().id, 108 secondaryMsgOverride = value 109 ) 110 } else { 111 null 112 } 113 ) 114 } 115 116 fun setCustomMessage(value: String?) { 117 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 118 119 repository.setCustomMessage( 120 if (value != null) { 121 factory.createFromPromptReason( 122 PROMPT_REASON_DEFAULT, 123 userRepository.getSelectedUserInfo().id, 124 secondaryMsgOverride = value 125 ) 126 } else { 127 null 128 } 129 ) 130 } 131 132 fun onPrimaryBouncerUserInput() { 133 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 134 135 repository.clearMessage() 136 } 137 138 fun onBouncerBeingHidden() { 139 if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return 140 141 repository.clearMessage() 142 } 143 144 private fun firstNonNullMessage( 145 oneMessageModel: Flow<BouncerMessageModel?>, 146 anotherMessageModel: Flow<BouncerMessageModel?> 147 ): Flow<BouncerMessageModel?> { 148 return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b } 149 } 150 151 // Null if feature flag is enabled which gets ignored always or empty bouncer message model that 152 // always maps to an empty string. 153 private fun nullOrEmptyMessage() = 154 flowOf( 155 if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null else factory.emptyMessage() 156 ) 157 158 val bouncerMessage = 159 listOf( 160 nullOrEmptyMessage(), 161 repository.primaryAuthMessage, 162 repository.biometricAuthMessage, 163 repository.fingerprintAcquisitionMessage, 164 repository.faceAcquisitionMessage, 165 repository.customMessage, 166 repository.authFlagsMessage, 167 repository.biometricLockedOutMessage, 168 userRepository.selectedUserInfo.map { 169 factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id) 170 }, 171 ) 172 .reduce(::firstNonNullMessage) 173 .distinctUntilChanged() 174 } 175 176 interface CountDownTimerCallback { 177 fun onFinish() 178 fun onTick(millisUntilFinished: Long) 179 } 180 181 @SysUISingleton 182 open class CountDownTimerUtil @Inject constructor() { 183 184 /** 185 * Start a new count down timer that runs for [millisInFuture] with a tick every 186 * [millisInterval] 187 */ 188 fun startNewTimer( 189 millisInFuture: Long, 190 millisInterval: Long, 191 callback: CountDownTimerCallback, 192 ): CountDownTimer { 193 return object : CountDownTimer(millisInFuture, millisInterval) { 194 override fun onFinish() = callback.onFinish() 195 196 override fun onTick(millisUntilFinished: Long) = 197 callback.onTick(millisUntilFinished) 198 } 199 .start() 200 } 201 } 202