diff options
27 files changed, 2173 insertions, 8 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt new file mode 100644 index 000000000000..6f3beac2ac85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt @@ -0,0 +1,54 @@ +/* + * 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.common.coroutine + +import android.util.Log +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.onFailure + +object ChannelExt { + + /** + * Convenience wrapper around [SendChannel.trySend] that also logs on failure. This is the + * equivalent of calling: + * + * ``` + * sendChannel.trySend(element).onFailure { + * Log.e( + * loggingTag, + * "Failed to send $elementDescription" + + * " - downstream canceled or failed.", + * it, + * ) + *} + * ``` + */ + fun <T> SendChannel<T>.trySendWithFailureLogging( + element: T, + loggingTag: String, + elementDescription: String = "updated state", + ) { + trySend(element).onFailure { + Log.e( + loggingTag, + "Failed to send $elementDescription - downstream canceled or failed.", + it, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt new file mode 100644 index 000000000000..d4a1f74234ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt @@ -0,0 +1,40 @@ +/* + * 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.common.coroutine + +import kotlin.experimental.ExperimentalTypeInference +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow + +object ConflatedCallbackFlow { + + /** + * A [callbackFlow] that uses a buffer [Channel] that is "conflated" meaning that, if + * backpressure occurs (if the producer that emits new values into the flow is faster than the + * consumer(s) of the values in the flow), the values are buffered and, if the buffer fills up, + * we drop the oldest values automatically instead of suspending the producer. + */ + @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") + @OptIn(ExperimentalTypeInference::class, ExperimentalCoroutinesApi::class) + fun <T> conflatedCallbackFlow( + @BuilderInference block: suspend ProducerScope<T>.() -> Unit, + ): Flow<T> = callbackFlow(block).buffer(capacity = Channel.CONFLATED) +} diff --git a/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt b/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt new file mode 100644 index 000000000000..7c9df102ef1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt @@ -0,0 +1,23 @@ +/* + * 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.common.data.model + +/** Models a two-dimensional position */ +data class Position( + val x: Int, + val y: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt new file mode 100644 index 000000000000..d6a059da3afa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt @@ -0,0 +1,27 @@ +/* + * 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.containeddrawable + +import android.graphics.drawable.Drawable +import androidx.annotation.DrawableRes + +/** Convenience container for [Drawable] or a way to load it later. */ +sealed class ContainedDrawable { + data class WithDrawable(val drawable: Drawable) : ContainedDrawable() + data class WithResource(@DrawableRes val resourceId: Int) : ContainedDrawable() +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt index 2fd373105745..9e4a364562e5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt @@ -21,20 +21,22 @@ import android.content.Context import android.database.ContentObserver import android.os.UserHandle import android.provider.Settings +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.controller.ControlsTileResourceConfiguration +import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.settings.SecureSettings -import com.android.internal.widget.LockPatternUtils -import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -import com.android.systemui.controls.controller.ControlsTileResourceConfiguration -import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow /** * Pseudo-component to inject into classes outside `com.android.systemui.controls`. @@ -59,7 +61,8 @@ class ControlsComponent @Inject constructor( private val contentResolver: ContentResolver get() = context.contentResolver - private var canShowWhileLockedSetting = false + private val _canShowWhileLockedSetting = MutableStateFlow(false) + val canShowWhileLockedSetting = _canShowWhileLockedSetting.asStateFlow() private val controlsTileResourceConfiguration: ControlsTileResourceConfiguration = optionalControlsTileResourceConfiguration.orElse( @@ -117,7 +120,7 @@ class ControlsComponent @Inject constructor( == STRONG_AUTH_REQUIRED_AFTER_BOOT) { return Visibility.AVAILABLE_AFTER_UNLOCK } - if (!canShowWhileLockedSetting && !keyguardStateController.isUnlocked()) { + if (!canShowWhileLockedSetting.value && !keyguardStateController.isUnlocked()) { return Visibility.AVAILABLE_AFTER_UNLOCK } @@ -125,7 +128,7 @@ class ControlsComponent @Inject constructor( } private fun updateShowWhileLocked() { - canShowWhileLockedSetting = secureSettings.getIntForUser( + _canShowWhileLockedSetting.value = secureSettings.getIntForUser( Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0, UserHandle.USER_CURRENT) != 0 } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 165af135a792..5b2d88b53c22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -42,6 +42,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -66,7 +67,10 @@ import dagger.Provides; KeyguardStatusBarViewComponent.class, KeyguardStatusViewComponent.class, KeyguardUserSwitcherComponent.class}, - includes = {FalsingModule.class}) + includes = { + FalsingModule.class, + KeyguardRepositoryModule.class, + }) public class KeyguardModule { /** * Provides our instance of KeyguardViewMediator which is considered optional. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..3202ecb9a287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,121 @@ +/* + * 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.data.quickaffordance + +import android.content.Context +import android.content.Intent +import androidx.annotation.DrawableRes +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.ui.ControlsActivity +import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.util.kotlin.getOrNull +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Home controls quick affordance data source. */ +@SysUISingleton +class HomeControlsKeyguardQuickAffordanceConfig +@Inject +constructor( + @Application context: Context, + private val component: ControlsComponent, +) : KeyguardQuickAffordanceConfig { + + private val appContext = context.applicationContext + + override val state: Flow<KeyguardQuickAffordanceConfig.State> = + stateInternal(component.getControlsListingController().getOrNull()) + + override fun onQuickAffordanceClicked( + animationController: ActivityLaunchAnimator.Controller?, + ): KeyguardQuickAffordanceConfig.OnClickedResult { + return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( + intent = + Intent(appContext, ControlsActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra( + ControlsUiController.EXTRA_ANIMATE, + true, + ), + canShowWhileLocked = component.canShowWhileLockedSetting.value, + ) + } + + private fun stateInternal( + listingController: ControlsListingController?, + ): Flow<KeyguardQuickAffordanceConfig.State> { + if (listingController == null) { + return flowOf(KeyguardQuickAffordanceConfig.State.Hidden) + } + + return conflatedCallbackFlow { + val callback = + object : ControlsListingController.ControlsListingCallback { + override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { + val favorites: List<StructureInfo>? = + component.getControlsController().getOrNull()?.getFavorites() + + trySendWithFailureLogging( + state( + isFeatureEnabled = component.isEnabled(), + hasFavorites = favorites?.isNotEmpty() == true, + hasServiceInfos = serviceInfos.isNotEmpty(), + iconResourceId = component.getTileImageId(), + ), + TAG, + ) + } + } + + listingController.addCallback(callback) + + awaitClose { listingController.removeCallback(callback) } + } + } + + private fun state( + isFeatureEnabled: Boolean, + hasFavorites: Boolean, + hasServiceInfos: Boolean, + @DrawableRes iconResourceId: Int?, + ): KeyguardQuickAffordanceConfig.State { + return if (isFeatureEnabled && hasFavorites && hasServiceInfos && iconResourceId != null) { + KeyguardQuickAffordanceConfig.State.Visible( + icon = ContainedDrawable.WithResource(iconResourceId), + contentDescriptionResourceId = component.getTileTitleId(), + ) + } else { + KeyguardQuickAffordanceConfig.State.Hidden + } + } + + companion object { + private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..67a776eddccb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -0,0 +1,73 @@ +/* + * 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.data.quickaffordance + +import android.content.Intent +import androidx.annotation.StringRes +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.containeddrawable.ContainedDrawable +import kotlinx.coroutines.flow.Flow + +/** Defines interface that can act as data source for a single quick affordance model. */ +interface KeyguardQuickAffordanceConfig { + + val state: Flow<State> + + fun onQuickAffordanceClicked( + animationController: ActivityLaunchAnimator.Controller? + ): OnClickedResult + + /** + * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a + * button on the lock-screen). + */ + sealed class State { + + /** No affordance should show up. */ + object Hidden : State() + + /** An affordance is visible. */ + data class Visible( + /** An icon for the affordance. */ + val icon: ContainedDrawable, + /** + * Resource ID for a string to use for the accessibility content description text of the + * affordance. + */ + @StringRes val contentDescriptionResourceId: Int, + ) : State() + } + + sealed class OnClickedResult { + /** + * Returning this as a result from the [onQuickAffordanceClicked] method means that the + * implementation has taken care of the click, the system will do nothing. + */ + object Handled : OnClickedResult() + + /** + * Returning this as a result from the [onQuickAffordanceClicked] method means that the + * implementation has _not_ taken care of the click and the system should start an activity + * using the given [Intent]. + */ + data class StartActivity( + val intent: Intent, + val canShowWhileLocked: Boolean, + ) : OnClickedResult() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..758e4114c77e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,95 @@ +/* + * 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.data.quickaffordance + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.qrcodescanner.controller.QRCodeScannerController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** QR code scanner quick affordance data source. */ +@SysUISingleton +class QrCodeScannerKeyguardQuickAffordanceConfig +@Inject +constructor( + @Application context: Context, + private val controller: QRCodeScannerController, +) : KeyguardQuickAffordanceConfig { + + private val appContext = context.applicationContext + + override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow { + val callback = + object : QRCodeScannerController.Callback { + override fun onQRCodeScannerActivityChanged() { + trySendWithFailureLogging(state(), TAG) + } + override fun onQRCodeScannerPreferenceChanged() { + trySendWithFailureLogging(state(), TAG) + } + } + + controller.addCallback(callback) + controller.registerQRCodeScannerChangeObservers( + QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, + QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE + ) + // Registering does not push an initial update. + trySendWithFailureLogging(state(), "initial state", TAG) + + awaitClose { + controller.unregisterQRCodeScannerChangeObservers( + QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, + QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE + ) + controller.removeCallback(callback) + } + } + + override fun onQuickAffordanceClicked( + animationController: ActivityLaunchAnimator.Controller?, + ): KeyguardQuickAffordanceConfig.OnClickedResult { + return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( + intent = controller.intent, + canShowWhileLocked = true, + ) + } + + private fun state(): KeyguardQuickAffordanceConfig.State { + return if (controller.isEnabledForLockScreenButton) { + KeyguardQuickAffordanceConfig.State.Visible( + icon = ContainedDrawable.WithResource(R.drawable.ic_qr_code_scanner), + contentDescriptionResourceId = R.string.accessibility_qr_code_scanner_button, + ) + } else { + KeyguardQuickAffordanceConfig.State.Hidden + } + } + + companion object { + private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..c686e27adb2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,134 @@ +/* + * 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.data.quickaffordance + +import android.graphics.drawable.Drawable +import android.service.quickaccesswallet.GetWalletCardsError +import android.service.quickaccesswallet.GetWalletCardsResponse +import android.service.quickaccesswallet.QuickAccessWalletClient +import android.util.Log +import com.android.systemui.R +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.KeyguardStateControllerExt.isKeyguardShowing +import com.android.systemui.wallet.controller.QuickAccessWalletController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Quick access wallet quick affordance data source. */ +@SysUISingleton +class QuickAccessWalletKeyguardQuickAffordanceConfig +@Inject +constructor( + private val keyguardStateController: KeyguardStateController, + private val walletController: QuickAccessWalletController, + private val activityStarter: ActivityStarter, +) : KeyguardQuickAffordanceConfig { + + override val state: Flow<KeyguardQuickAffordanceConfig.State> = + keyguardStateController + .isKeyguardShowing(TAG) + .flatMapLatest { isKeyguardShowing -> + stateInternal(isKeyguardShowing) + } + + override fun onQuickAffordanceClicked( + animationController: ActivityLaunchAnimator.Controller?, + ): KeyguardQuickAffordanceConfig.OnClickedResult { + walletController.startQuickAccessUiIntent( + activityStarter, + animationController, + /* hasCard= */ true, + ) + return KeyguardQuickAffordanceConfig.OnClickedResult.Handled + } + + private fun stateInternal( + isKeyguardShowing: Boolean + ): Flow<KeyguardQuickAffordanceConfig.State> { + if (!isKeyguardShowing) { + return flowOf(KeyguardQuickAffordanceConfig.State.Hidden) + } + + return conflatedCallbackFlow { + val callback = + object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { + override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) { + trySendWithFailureLogging( + state( + isFeatureEnabled = walletController.isWalletEnabled, + hasCard = response?.walletCards?.isNotEmpty() == true, + tileIcon = walletController.walletClient.tileIcon, + ), + TAG, + ) + } + + override fun onWalletCardRetrievalError(error: GetWalletCardsError?) { + Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"") + trySendWithFailureLogging( + KeyguardQuickAffordanceConfig.State.Hidden, + TAG, + ) + } + } + + walletController.setupWalletChangeObservers( + callback, + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + walletController.updateWalletPreference() + walletController.queryWalletCards(callback) + + awaitClose { + walletController.unregisterWalletChangeObservers( + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + } + } + } + + private fun state( + isFeatureEnabled: Boolean, + hasCard: Boolean, + tileIcon: Drawable?, + ): KeyguardQuickAffordanceConfig.State { + return if (isFeatureEnabled && hasCard && tileIcon != null) { + KeyguardQuickAffordanceConfig.State.Visible( + icon = ContainedDrawable.WithDrawable(tileIcon), + contentDescriptionResourceId = R.string.accessibility_wallet_button, + ) + } else { + KeyguardQuickAffordanceConfig.State.Hidden + } + } + + companion object { + private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt new file mode 100644 index 000000000000..7164215eb2ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt @@ -0,0 +1,67 @@ +/* + * 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.data.config + +import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition +import javax.inject.Inject +import kotlin.reflect.KClass + +/** Injectable provider of the positioning of the known quick affordance configs. */ +interface KeyguardQuickAffordanceConfigs { + fun getAll(position: KeyguardQuickAffordancePosition): List<KeyguardQuickAffordanceConfig> + fun get(configClass: KClass<out KeyguardQuickAffordanceConfig>): KeyguardQuickAffordanceConfig +} + +class KeyguardQuickAffordanceConfigsImpl +@Inject +constructor( + homeControls: HomeControlsKeyguardQuickAffordanceConfig, + quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, + qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, +) : KeyguardQuickAffordanceConfigs { + private val configsByPosition = + mapOf( + KeyguardQuickAffordancePosition.BOTTOM_START to + listOf( + homeControls, + ), + KeyguardQuickAffordancePosition.BOTTOM_END to + listOf( + quickAccessWallet, + qrCodeScanner, + ), + ) + private val configByClass = + configsByPosition.values.flatten().associateBy { config -> config::class } + + override fun getAll( + position: KeyguardQuickAffordancePosition, + ): List<KeyguardQuickAffordanceConfig> { + return configsByPosition.getValue(position) + } + + override fun get( + configClass: KClass<out KeyguardQuickAffordanceConfig> + ): KeyguardQuickAffordanceConfig { + return configByClass.getValue(configClass) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt new file mode 100644 index 000000000000..43c4fa06367b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -0,0 +1,65 @@ +/* + * 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.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.State +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** Defines interface for classes that encapsulate quick affordance state for the keyguard. */ +interface KeyguardQuickAffordanceRepository { + fun affordance(position: KeyguardQuickAffordancePosition): Flow<KeyguardQuickAffordanceModel> +} + +/** Real implementation of [KeyguardQuickAffordanceRepository] */ +@SysUISingleton +class KeyguardQuickAffordanceRepositoryImpl +@Inject +constructor( + private val configs: KeyguardQuickAffordanceConfigs, +) : KeyguardQuickAffordanceRepository { + + /** Returns an observable for the quick affordance model in the given position. */ + override fun affordance( + position: KeyguardQuickAffordancePosition + ): Flow<KeyguardQuickAffordanceModel> { + val configs = configs.getAll(position) + return combine(configs.map { config -> config.state }) { states -> + val index = states.indexOfFirst { state -> state is State.Visible } + val visibleState = + if (index != -1) { + states[index] as State.Visible + } else { + null + } + if (visibleState != null) { + KeyguardQuickAffordanceModel.Visible( + configKey = configs[index]::class, + icon = visibleState.icon, + contentDescriptionResourceId = visibleState.contentDescriptionResourceId, + ) + } else { + KeyguardQuickAffordanceModel.Hidden + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt new file mode 100644 index 000000000000..be91e51f8550 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -0,0 +1,147 @@ +/* + * 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.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.data.model.Position +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.statusbar.StatusBarStateController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Defines interface for classes that encapsulate application state for the keyguard. */ +interface KeyguardRepository { + /** + * Observable for whether the bottom area UI should animate the transition out of doze state. + * + * To learn more about doze state, please see [isDozing]. + */ + val animateBottomAreaDozingTransitions: StateFlow<Boolean> + + /** + * Observable for the current amount of alpha that should be used for rendering the bottom area. + * UI. + */ + val bottomAreaAlpha: StateFlow<Float> + + /** + * Observable of the relative offset of the lock-screen clock from its natural position on the + * screen. + */ + val clockPosition: StateFlow<Position> + + /** + * Observable for whether we are in doze state. + * + * Doze state is the same as "Always on Display" or "AOD". It is the state that the device can + * enter to conserve battery when the device is locked and inactive. + * + * Note that it is possible for the system to be transitioning into doze while this flow still + * returns `false`. In order to account for that, observers should also use the [dozeAmount] + * flow to check if it's greater than `0` + */ + val isDozing: Flow<Boolean> + + /** + * Observable for the amount of doze we are currently in. + * + * While in doze state, this amount can change - driving a cycle of animations designed to avoid + * pixel burn-in, etc. + * + * Also note that the value here may be greater than `0` while [isDozing] is still `false`, this + * happens during an animation/transition into doze mode. An observer would be wise to account + * for both flows if needed. + */ + val dozeAmount: Flow<Float> + + /** Sets whether the bottom area UI should animate the transition out of doze state. */ + fun setAnimateDozingTransitions(animate: Boolean) + + /** Sets the current amount of alpha that should be used for rendering the bottom area. */ + fun setBottomAreaAlpha(alpha: Float) + + /** + * Sets the relative offset of the lock-screen clock from its natural position on the screen. + */ + fun setClockPosition(x: Int, y: Int) +} + +/** Encapsulates application state for the keyguard. */ +@SysUISingleton +class KeyguardRepositoryImpl +@Inject +constructor( + statusBarStateController: StatusBarStateController, +) : KeyguardRepository { + private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) + override val animateBottomAreaDozingTransitions = + _animateBottomAreaDozingTransitions.asStateFlow() + + private val _bottomAreaAlpha = MutableStateFlow(1f) + override val bottomAreaAlpha = _bottomAreaAlpha.asStateFlow() + + private val _clockPosition = MutableStateFlow(Position(0, 0)) + override val clockPosition = _clockPosition.asStateFlow() + + override val isDozing: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + trySendWithFailureLogging(isDozing, TAG, "updated isDozing") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging(statusBarStateController.isDozing, TAG, "initial isDozing") + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override val dozeAmount: Flow<Float> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onDozeAmountChanged(linear: Float, eased: Float) { + trySendWithFailureLogging(eased, TAG, "updated dozeAmount") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging(statusBarStateController.dozeAmount, TAG, "initial dozeAmount") + + awaitClose { statusBarStateController.removeCallback(callback) } + } + + override fun setAnimateDozingTransitions(animate: Boolean) { + _animateBottomAreaDozingTransitions.value = animate + } + + override fun setBottomAreaAlpha(alpha: Float) { + _bottomAreaAlpha.value = alpha + } + + override fun setClockPosition(x: Int, y: Int) { + _clockPosition.value = Position(x, y) + } + + companion object { + private const val TAG = "KeyguardRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt new file mode 100644 index 000000000000..d2ab3e9b0f87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -0,0 +1,36 @@ +/* + * 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.data.repository + +import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs +import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigsImpl +import dagger.Binds +import dagger.Module + +@Module +interface KeyguardRepositoryModule { + @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository + + @Binds + fun keyguardQuickAffordanceRepository( + impl: KeyguardQuickAffordanceRepositoryImpl + ): KeyguardQuickAffordanceRepository + + @Binds fun keyguardQuickAffordanceConfigs( + impl: KeyguardQuickAffordanceConfigsImpl + ): KeyguardQuickAffordanceConfigs +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt new file mode 100644 index 000000000000..09785dfa3c03 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt @@ -0,0 +1,46 @@ +/* + * 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.shared.model + +import androidx.annotation.StringRes +import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import kotlin.reflect.KClass + +/** + * Models a "quick affordance" in the keyguard bottom area (for example, a button on the + * lock-screen). + */ +sealed class KeyguardQuickAffordanceModel { + + /** No affordance should show up. */ + object Hidden : KeyguardQuickAffordanceModel() + + /** A affordance is visible. */ + data class Visible( + /** Identifier for the affordance this is modeling. */ + val configKey: KClass<out KeyguardQuickAffordanceConfig>, + /** An icon for the affordance. */ + val icon: ContainedDrawable, + /** + * Resource ID for a string to use for the accessibility content description text of the + * affordance. + */ + @StringRes val contentDescriptionResourceId: Int, + ) : KeyguardQuickAffordanceModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt new file mode 100644 index 000000000000..b71e15d34afe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt @@ -0,0 +1,23 @@ +/* + * 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.shared.model + +/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */ +enum class KeyguardQuickAffordancePosition { + BOTTOM_START, + BOTTOM_END, +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerExt.kt new file mode 100644 index 000000000000..b3f1eebb420b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerExt.kt @@ -0,0 +1,46 @@ +/* + * 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.statusbar.policy + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +object KeyguardStateControllerExt { + /** + * Returns an observable for whether the keyguard is currently shown or not. + */ + fun KeyguardStateController.isKeyguardShowing(loggingTag: String): Flow<Boolean> { + return ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + trySendWithFailureLogging( + isShowing, loggingTag, "updated isKeyguardShowing") + } + } + + addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging(isShowing, loggingTag, "initial isKeyguardShowing") + + awaitClose { removeCallback(callback) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..6fff440ec2fa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,60 @@ +/* + * 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.data.repository + +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.yield + +/** + * Fake implementation of a quick affordance data source. + * + * This class is abstract to force tests to provide extensions of it as the system that references + * these configs uses each implementation's class type to refer to them. + */ +abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig { + + private val _onClickedInvocations = mutableListOf<ActivityLaunchAnimator.Controller?>() + val onClickedInvocations: List<ActivityLaunchAnimator.Controller?> = _onClickedInvocations + + var onClickedResult: OnClickedResult = OnClickedResult.Handled + + private val _state = + MutableStateFlow<KeyguardQuickAffordanceConfig.State>( + KeyguardQuickAffordanceConfig.State.Hidden + ) + override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state + + override fun onQuickAffordanceClicked( + animationController: ActivityLaunchAnimator.Controller?, + ): OnClickedResult { + _onClickedInvocations.add(animationController) + return onClickedResult + } + + suspend fun setState(state: KeyguardQuickAffordanceConfig.State) { + _state.value = state + // Yield to allow the test's collection coroutine to "catch up" and collect this value + // before the test continues to the next line. + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this. + yield() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt new file mode 100644 index 000000000000..a24fc93fedc2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt @@ -0,0 +1,44 @@ +/* + * 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.data.repository + +import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition +import kotlin.reflect.KClass + +/** Fake implementation of [KeyguardQuickAffordanceConfigs], for tests. */ +class FakeKeyguardQuickAffordanceConfigs( + private val configsByPosition: + Map<KeyguardQuickAffordancePosition, List<KeyguardQuickAffordanceConfig>>, +) : KeyguardQuickAffordanceConfigs { + + override fun getAll( + position: KeyguardQuickAffordancePosition + ): List<KeyguardQuickAffordanceConfig> { + return configsByPosition.getValue(position) + } + + override fun get( + configClass: KClass<out KeyguardQuickAffordanceConfig> + ): KeyguardQuickAffordanceConfig { + return configsByPosition.values + .flatten() + .associateBy { config -> config::class } + .getValue(configClass) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt new file mode 100644 index 000000000000..7d1cccb8a0a8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt @@ -0,0 +1,55 @@ +/* + * 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.data.repository + +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.yield + +/** Fake implementation of [KeyguardQuickAffordanceRepository], for tests. */ +class FakeKeyguardQuickAffordanceRepository : KeyguardQuickAffordanceRepository { + + private val modelByPosition = + mutableMapOf< + KeyguardQuickAffordancePosition, MutableStateFlow<KeyguardQuickAffordanceModel>>() + + init { + KeyguardQuickAffordancePosition.values().forEach { value -> + modelByPosition[value] = MutableStateFlow(KeyguardQuickAffordanceModel.Hidden) + } + } + + override fun affordance( + position: KeyguardQuickAffordancePosition + ): Flow<KeyguardQuickAffordanceModel> { + return modelByPosition.getValue(position) + } + + suspend fun setModel( + position: KeyguardQuickAffordancePosition, + model: KeyguardQuickAffordanceModel + ) { + modelByPosition.getValue(position).value = model + // Yield to allow the test's collection coroutine to "catch up" and collect this value + // before the test continues to the next line. + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this. + yield() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt new file mode 100644 index 000000000000..c82803af4f37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -0,0 +1,77 @@ +/* + * 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.data.repository + +import com.android.systemui.common.data.model.Position +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fake implementation of [KeyguardRepository] */ +class FakeKeyguardRepository : KeyguardRepository { + + private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) + override val animateBottomAreaDozingTransitions: StateFlow<Boolean> = + _animateBottomAreaDozingTransitions + + private val _bottomAreaAlpha = MutableStateFlow(1f) + override val bottomAreaAlpha: StateFlow<Float> = _bottomAreaAlpha + + private val _clockPosition = MutableStateFlow(Position(0, 0)) + override val clockPosition: StateFlow<Position> = _clockPosition + + private val _isDozing = + MutableSharedFlow<Boolean>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val isDozing: Flow<Boolean> = _isDozing + + private val _dozeAmount = + MutableSharedFlow<Float>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val dozeAmount: Flow<Float> = _dozeAmount + + init { + setDozeAmount(0f) + setDozing(false) + } + + override fun setAnimateDozingTransitions(animate: Boolean) { + _animateBottomAreaDozingTransitions.tryEmit(animate) + } + + override fun setBottomAreaAlpha(alpha: Float) { + _bottomAreaAlpha.value = alpha + } + + override fun setClockPosition(x: Int, y: Int) { + _clockPosition.value = Position(x, y) + } + + fun setDozing(isDozing: Boolean) { + _isDozing.tryEmit(isDozing) + } + + fun setDozeAmount(dozeAmount: Float) { + _dozeAmount.tryEmit(dozeAmount) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt new file mode 100644 index 000000000000..bcc76abc89ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -0,0 +1,133 @@ +/* + * 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Parameterized::class) +class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() { + + companion object { + @Parameters( + name = + "feature enabled = {0}, has favorites = {1}, has service infos = {2} - expected" + + " visible = {3}" + ) + @JvmStatic + fun data() = + (0 until 8) + .map { combination -> + arrayOf( + /* isFeatureEnabled= */ combination and 0b100 != 0, + /* hasFavorites= */ combination and 0b010 != 0, + /* hasServiceInfos= */ combination and 0b001 != 0, + /* isVisible= */ combination == 0b111, + ) + } + .toList() + } + + @Mock private lateinit var component: ControlsComponent + @Mock private lateinit var controlsController: ControlsController + @Mock private lateinit var controlsListingController: ControlsListingController + @Captor + private lateinit var callbackCaptor: + ArgumentCaptor<ControlsListingController.ControlsListingCallback> + + private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig + + @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false + @JvmField @Parameter(1) var hasFavorites: Boolean = false + @JvmField @Parameter(2) var hasServiceInfos: Boolean = false + @JvmField @Parameter(3) var isVisible: Boolean = false + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon) + whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title) + whenever(component.getControlsController()).thenReturn(Optional.of(controlsController)) + whenever(component.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + + underTest = + HomeControlsKeyguardQuickAffordanceConfig( + context = context, + component = component, + ) + } + + @Test + fun state() = runBlockingTest { + whenever(component.isEnabled()).thenReturn(isFeatureEnabled) + whenever(controlsController.getFavorites()) + .thenReturn( + if (hasFavorites) { + listOf(mock()) + } else { + emptyList() + } + ) + val values = mutableListOf<KeyguardQuickAffordanceConfig.State>() + val job = underTest.state.onEach(values::add).launchIn(this) + + verify(controlsListingController).addCallback(callbackCaptor.capture()) + callbackCaptor.value.onServicesUpdated( + if (hasServiceInfos) { + listOf(mock()) + } else { + emptyList() + } + ) + + assertThat(values.last()) + .isInstanceOf( + if (isVisible) { + KeyguardQuickAffordanceConfig.State.Visible::class.java + } else { + KeyguardQuickAffordanceConfig.State.Hidden::class.java + } + ) + job.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..9ce572437b91 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigTest.kt @@ -0,0 +1,100 @@ +/* + * 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var component: ControlsComponent + @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller + + private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + HomeControlsKeyguardQuickAffordanceConfig( + context = context, + component = component, + ) + } + + @Test + fun `state - when listing controller is missing - returns None`() = runBlockingTest { + whenever(component.isEnabled()).thenReturn(true) + whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon) + whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title) + val controlsController = mock<ControlsController>() + whenever(component.getControlsController()).thenReturn(Optional.of(controlsController)) + whenever(component.getControlsListingController()).thenReturn(Optional.empty()) + whenever(controlsController.getFavorites()).thenReturn(listOf(mock())) + + val values = mutableListOf<KeyguardQuickAffordanceConfig.State>() + val job = underTest.state.onEach(values::add).launchIn(this) + + assertThat(values.last()) + .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java) + job.cancel() + } + + @Test + fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest { + whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true)) + + val onClickedResult = underTest.onQuickAffordanceClicked(animationController) + + assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) + assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue() + } + + @Test + fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest { + whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false)) + + val onClickedResult = underTest.onQuickAffordanceClicked(animationController) + + assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) + assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt new file mode 100644 index 000000000000..dc0e6f7663ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt @@ -0,0 +1,193 @@ +/* + * 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.reflect.KClass +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardQuickAffordanceRepository + + private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig + private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig + private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + homeControls = object : FakeKeyguardQuickAffordanceConfig() {} + quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {} + qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {} + + underTest = + KeyguardQuickAffordanceRepositoryImpl( + configs = + FakeKeyguardQuickAffordanceConfigs( + mapOf( + KeyguardQuickAffordancePosition.BOTTOM_START to + listOf( + homeControls, + ), + KeyguardQuickAffordancePosition.BOTTOM_END to + listOf( + quickAccessWallet, + qrCodeScanner, + ), + ), + ), + ) + } + + @Test + fun `bottom start affordance - none`() = runBlockingTest { + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection + var latest: KeyguardQuickAffordanceModel? = null + val job = + underTest + .affordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { latest = it } + .launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + job.cancel() + } + + @Test + fun `bottom start affordance - home controls`() = runBlockingTest { + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection + var latest: KeyguardQuickAffordanceModel? = null + val job = + underTest + .affordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { latest = it } + .launchIn(this) + + val state = + KeyguardQuickAffordanceConfig.State.Visible( + icon = mock(), + contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + homeControls.setState(state) + + assertThat(latest).isEqualTo(state.toModel(homeControls::class)) + job.cancel() + } + + @Test + fun `bottom end affordance - none`() = runBlockingTest { + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection + var latest: KeyguardQuickAffordanceModel? = null + val job = + underTest + .affordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { latest = it } + .launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + job.cancel() + } + + @Test + fun `bottom end affordance - quick access wallet`() = runBlockingTest { + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection + var latest: KeyguardQuickAffordanceModel? = null + val job = + underTest + .affordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { latest = it } + .launchIn(this) + + val quickAccessWalletState = + KeyguardQuickAffordanceConfig.State.Visible( + icon = mock(), + contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + quickAccessWallet.setState(quickAccessWalletState) + val qrCodeScannerState = + KeyguardQuickAffordanceConfig.State.Visible( + icon = mock(), + contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + qrCodeScanner.setState(qrCodeScannerState) + + assertThat(latest).isEqualTo(quickAccessWalletState.toModel(quickAccessWallet::class)) + job.cancel() + } + + @Test + fun `bottom end affordance - qr code scanner`() = runBlockingTest { + // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in + // https://developer.android.com/kotlin/flow/test#continuous-collection + var latest: KeyguardQuickAffordanceModel? = null + val job = + underTest + .affordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { latest = it } + .launchIn(this) + + val state = + KeyguardQuickAffordanceConfig.State.Visible( + icon = mock(), + contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + qrCodeScanner.setState(state) + + assertThat(latest).isEqualTo(state.toModel(qrCodeScanner::class)) + job.cancel() + } + + private fun KeyguardQuickAffordanceConfig.State?.toModel( + configKey: KClass<out KeyguardQuickAffordanceConfig>, + ): KeyguardQuickAffordanceModel? { + return when (this) { + is KeyguardQuickAffordanceConfig.State.Visible -> + KeyguardQuickAffordanceModel.Visible( + configKey = configKey, + icon = icon, + contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + is KeyguardQuickAffordanceConfig.State.Hidden -> KeyguardQuickAffordanceModel.Hidden + null -> null + } + } + + companion object { + private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt new file mode 100644 index 000000000000..2ee80349ff4c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -0,0 +1,137 @@ +/* + * 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.model.Position +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.util.mockito.argumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var statusBarStateController: StatusBarStateController + + private lateinit var underTest: KeyguardRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = KeyguardRepositoryImpl(statusBarStateController) + } + + @Test + fun animateBottomAreaDozingTransitions() = runBlockingTest { + assertThat(underTest.animateBottomAreaDozingTransitions.value).isEqualTo(false) + + underTest.setAnimateDozingTransitions(true) + assertThat(underTest.animateBottomAreaDozingTransitions.value).isTrue() + + underTest.setAnimateDozingTransitions(false) + assertThat(underTest.animateBottomAreaDozingTransitions.value).isFalse() + + underTest.setAnimateDozingTransitions(true) + assertThat(underTest.animateBottomAreaDozingTransitions.value).isTrue() + } + + @Test + fun bottomAreaAlpha() = runBlockingTest { + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f) + + underTest.setBottomAreaAlpha(0.1f) + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.1f) + + underTest.setBottomAreaAlpha(0.2f) + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.2f) + + underTest.setBottomAreaAlpha(0.3f) + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.3f) + + underTest.setBottomAreaAlpha(0.5f) + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.5f) + + underTest.setBottomAreaAlpha(1.0f) + assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f) + } + + @Test + fun clockPosition() = runBlockingTest { + assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0)) + + underTest.setClockPosition(0, 1) + assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 1)) + + underTest.setClockPosition(1, 9) + assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 9)) + + underTest.setClockPosition(1, 0) + assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 0)) + + underTest.setClockPosition(3, 1) + assertThat(underTest.clockPosition.value).isEqualTo(Position(3, 1)) + } + + @Test + fun isDozing() = runBlockingTest { + var latest: Boolean? = null + val job = underTest.isDozing.onEach { latest = it }.launchIn(this) + + val captor = argumentCaptor<StatusBarStateController.StateListener>() + verify(statusBarStateController).addCallback(captor.capture()) + + captor.value.onDozingChanged(true) + assertThat(latest).isTrue() + + captor.value.onDozingChanged(false) + assertThat(latest).isFalse() + + job.cancel() + verify(statusBarStateController).removeCallback(captor.value) + } + + @Test + fun dozeAmount() = runBlockingTest { + val values = mutableListOf<Float>() + val job = underTest.dozeAmount.onEach(values::add).launchIn(this) + + val captor = argumentCaptor<StatusBarStateController.StateListener>() + verify(statusBarStateController).addCallback(captor.capture()) + + captor.value.onDozeAmountChanged(0.433f, 0.4f) + captor.value.onDozeAmountChanged(0.498f, 0.5f) + captor.value.onDozeAmountChanged(0.661f, 0.65f) + + assertThat(values).isEqualTo(listOf(0f, 0.4f, 0.5f, 0.65f)) + + job.cancel() + verify(statusBarStateController).removeCallback(captor.value) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..4bef00a13a07 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -0,0 +1,149 @@ +/* + * 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.data.repository + +import android.content.Intent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig +import com.android.systemui.qrcodescanner.controller.QRCodeScannerController +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var controller: QRCodeScannerController + + private lateinit var underTest: QrCodeScannerKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(controller.intent).thenReturn(INTENT_1) + + underTest = QrCodeScannerKeyguardQuickAffordanceConfig(context, controller) + } + + @Test + fun `affordance - sets up registration and delivers initial model`() = runBlockingTest { + whenever(controller.isEnabledForLockScreenButton).thenReturn(true) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() + verify(controller).addCallback(callbackCaptor.capture()) + verify(controller) + .registerQRCodeScannerChangeObservers( + QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, + QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE + ) + assertVisibleState(latest) + + job.cancel() + verify(controller).removeCallback(callbackCaptor.value) + } + + @Test + fun `affordance - scanner activity changed - delivers model with updated intent`() = + runBlockingTest { + whenever(controller.isEnabledForLockScreenButton).thenReturn(true) + var latest: KeyguardQuickAffordanceConfig.State? = null + val job = underTest.state.onEach { latest = it }.launchIn(this) + val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() + verify(controller).addCallback(callbackCaptor.capture()) + + whenever(controller.intent).thenReturn(INTENT_2) + callbackCaptor.value.onQRCodeScannerActivityChanged() + + assertVisibleState(latest) + + job.cancel() + verify(controller).removeCallback(callbackCaptor.value) + } + + @Test + fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest { + var latest: KeyguardQuickAffordanceConfig.State? = null + val job = underTest.state.onEach { latest = it }.launchIn(this) + val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() + verify(controller).addCallback(callbackCaptor.capture()) + + whenever(controller.isEnabledForLockScreenButton).thenReturn(true) + callbackCaptor.value.onQRCodeScannerPreferenceChanged() + + assertVisibleState(latest) + + job.cancel() + verify(controller).removeCallback(callbackCaptor.value) + } + + @Test + fun `affordance - scanner preference changed - delivers none`() = runBlockingTest { + var latest: KeyguardQuickAffordanceConfig.State? = null + val job = underTest.state.onEach { latest = it }.launchIn(this) + val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() + verify(controller).addCallback(callbackCaptor.capture()) + + whenever(controller.isEnabledForLockScreenButton).thenReturn(false) + callbackCaptor.value.onQRCodeScannerPreferenceChanged() + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + verify(controller).removeCallback(callbackCaptor.value) + } + + @Test + fun onQuickAffordanceClicked() { + assertThat(underTest.onQuickAffordanceClicked(mock())) + .isEqualTo( + OnClickedResult.StartActivity( + intent = INTENT_1, + canShowWhileLocked = true, + ) + ) + } + + private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.State?) { + assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java) + val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible + assertThat(visibleState.icon).isNotNull() + assertThat(visibleState.contentDescriptionResourceId).isNotNull() + } + + companion object { + private val INTENT_1 = Intent("intent1") + private val INTENT_2 = Intent("intent2") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..ee1d4d895e59 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -0,0 +1,213 @@ +/* + * 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.data.repository + +import android.graphics.drawable.Drawable +import android.service.quickaccesswallet.GetWalletCardsResponse +import android.service.quickaccesswallet.QuickAccessWalletClient +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.wallet.controller.QuickAccessWalletController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var walletController: QuickAccessWalletController + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var activityStarter: ActivityStarter + + private lateinit var underTest: QuickAccessWalletKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + QuickAccessWalletKeyguardQuickAffordanceConfig( + keyguardStateController, + walletController, + activityStarter, + ) + } + + @Test + fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest { + val callback = setUpState() + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible + assertThat(visibleModel.icon).isEqualTo(ContainedDrawable.WithDrawable(ICON)) + assertThat(visibleModel.contentDescriptionResourceId).isNotNull() + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun `affordance - keyguard not showing - model is none`() = runBlockingTest { + val callback = setUpState(isKeyguardShowing = false) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun `affordance - wallet not enabled - model is none`() = runBlockingTest { + val callback = setUpState(isWalletEnabled = false) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun `affordance - query not successful - model is none`() = runBlockingTest { + val callback = setUpState(isWalletQuerySuccessful = false) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun `affordance - missing icon - model is none`() = runBlockingTest { + val callback = setUpState(hasWalletIcon = false) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun `affordance - no selected card - model is none`() = runBlockingTest { + val callback = setUpState(hasWalletIcon = false) + var latest: KeyguardQuickAffordanceConfig.State? = null + + val job = underTest.state.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + + job.cancel() + callback?.let { verify(keyguardStateController).removeCallback(it) } + } + + @Test + fun onQuickAffordanceClicked() { + val animationController: ActivityLaunchAnimator.Controller = mock() + + assertThat(underTest.onQuickAffordanceClicked(animationController)) + .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled) + verify(walletController) + .startQuickAccessUiIntent( + activityStarter, + animationController, + /* hasCard= */ true, + ) + } + + private fun setUpState( + isKeyguardShowing: Boolean = true, + isWalletEnabled: Boolean = true, + isWalletQuerySuccessful: Boolean = true, + hasWalletIcon: Boolean = true, + hasSelectedCard: Boolean = true, + ): KeyguardStateController.Callback? { + var returnedCallback: KeyguardStateController.Callback? = null + whenever(keyguardStateController.isShowing).thenReturn(isKeyguardShowing) + whenever(keyguardStateController.addCallback(any())).thenAnswer { invocation -> + with(invocation.arguments[0] as KeyguardStateController.Callback) { + returnedCallback = this + onKeyguardShowingChanged() + } + } + + whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled) + + val walletClient: QuickAccessWalletClient = mock() + val icon: Drawable? = + if (hasWalletIcon) { + ICON + } else { + null + } + whenever(walletClient.tileIcon).thenReturn(icon) + whenever(walletController.walletClient).thenReturn(walletClient) + + whenever(walletController.queryWalletCards(any())).thenAnswer { invocation -> + with( + invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback + ) { + if (isWalletQuerySuccessful) { + onWalletCardsRetrieved( + if (hasSelectedCard) { + GetWalletCardsResponse(listOf(mock()), 0) + } else { + GetWalletCardsResponse(emptyList(), 0) + } + ) + } else { + onWalletCardRetrievalError(mock()) + } + } + } + + return returnedCallback + } + + companion object { + private val ICON: Drawable = mock() + } +} |