From 39fcf4f887172777f73992f897120ca6c5bac135 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 20 Jul 2022 19:40:25 -0700 Subject: [PATCH] KeyguardBottomAreaView new impl. - data layer - In this first CL in the series, we create the data layer that will power the new implementation of KeyguardBottomAreaView - On its own, this CL doesn't do anything and nothing is using it - The data layer contains both the repositories and models - There are two repositories: - The generic KeyguardRepository is responsible for providing generic application state that the view will ultimately consume. Note that some states are very specific to the bottom area and/or the view - this was done mostly as a shortcut because the current codebase makes it intractable to trace the true source of truth for these. Later, when things get cleaned up across the codebase, the repository can be changed to source the state from the real source of truth without making changes to its consumers - The specific KeyguardQuickAffordanceRepository is responsible for providing "quick affordance" (previously known as "bottom button") state. It creates a simple framework for defining the configuration for each of these buttons, positioning them on the left/right, and deciding which one takes precedence when more than on is available at the same position on the screen Bug: 235403546 Test: Unit tests are included for both repositories and for two out of the three quick affordance data sources. It was not possible to add a unit test for the "home controls" data source because of my use of `combine` which seems to not play nice with test coroutine scopes. I also manually verified that the bottom area behaves as expected once the entire CL chain is compiled together Change-Id: I90f8ccf0233c8a089b564a831f14157bf2096c31 --- .../systemui/common/coroutine/ChannelExt.kt | 54 +++++ .../common/coroutine/ConflatedCallbackFlow.kt | 40 ++++ .../systemui/common/data/model/Position.kt | 23 ++ .../containeddrawable/ContainedDrawable.kt | 27 +++ .../controls/dagger/ControlsComponent.kt | 17 +- .../keyguard/dagger/KeyguardModule.java | 6 +- ...meControlsKeyguardQuickAffordanceConfig.kt | 121 ++++++++++ .../KeyguardQuickAffordanceConfig.kt | 73 ++++++ ...odeScannerKeyguardQuickAffordanceConfig.kt | 95 ++++++++ ...cessWalletKeyguardQuickAffordanceConfig.kt | 134 +++++++++++ .../KeyguardQuickAffordanceConfigs.kt | 67 ++++++ .../KeyguardQuickAffordanceRepository.kt | 65 ++++++ .../data/repository/KeyguardRepository.kt | 147 ++++++++++++ .../repository/KeyguardRepositoryModule.kt | 36 +++ .../model/KeyguardQuickAffordanceModel.kt | 46 ++++ .../model/KeyguardQuickAffordancePosition.kt | 23 ++ .../policy/KeyguardStateControllerExt.kt | 46 ++++ .../FakeKeyguardQuickAffordanceConfig.kt | 60 +++++ .../FakeKeyguardQuickAffordanceConfigs.kt | 44 ++++ .../FakeKeyguardQuickAffordanceRepository.kt | 55 +++++ .../data/repository/FakeKeyguardRepository.kt | 77 +++++++ ...kAffordanceConfigParameterizedStateTest.kt | 133 +++++++++++ ...ntrolsKeyguardQuickAffordanceConfigTest.kt | 100 ++++++++ ...yguardQuickAffordanceRepositoryImplTest.kt | 193 ++++++++++++++++ .../repository/KeyguardRepositoryImplTest.kt | 137 +++++++++++ ...cannerKeyguardQuickAffordanceConfigTest.kt | 149 ++++++++++++ ...WalletKeyguardQuickAffordanceConfigTest.kt | 213 ++++++++++++++++++ 27 files changed, 2173 insertions(+), 8 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt create mode 100644 packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt create mode 100644 packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt create mode 100644 packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerExt.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt 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 0000000000000..6f3beac2ac85b --- /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 SendChannel.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 0000000000000..d4a1f74234ef0 --- /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 conflatedCallbackFlow( + @BuilderInference block: suspend ProducerScope.() -> Unit, + ): Flow = 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 0000000000000..7c9df102ef1dc --- /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 0000000000000..d6a059da3afa9 --- /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 2fd3731057456..9e4a364562e5e 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 165af135a792f..5b2d88b53c220 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 0000000000000..3202ecb9a2870 --- /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 = + 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 { + if (listingController == null) { + return flowOf(KeyguardQuickAffordanceConfig.State.Hidden) + } + + return conflatedCallbackFlow { + val callback = + object : ControlsListingController.ControlsListingCallback { + override fun onServicesUpdated(serviceInfos: List) { + val favorites: List? = + 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 0000000000000..67a776eddccb0 --- /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 + + 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 0000000000000..758e4114c77eb --- /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 = 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 0000000000000..c686e27adb2ac --- /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 = + 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 { + 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 0000000000000..7164215eb2ae0 --- /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 + fun get(configClass: KClass): 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 { + return configsByPosition.getValue(position) + } + + override fun get( + configClass: KClass + ): 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 0000000000000..43c4fa06367bc --- /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 +} + +/** 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 { + 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 0000000000000..be91e51f85508 --- /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 + + /** + * Observable for the current amount of alpha that should be used for rendering the bottom area. + * UI. + */ + val bottomAreaAlpha: StateFlow + + /** + * Observable of the relative offset of the lock-screen clock from its natural position on the + * screen. + */ + val clockPosition: StateFlow + + /** + * 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 + + /** + * 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 + + /** 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 = 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 = 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 0000000000000..d2ab3e9b0f879 --- /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 0000000000000..09785dfa3c03e --- /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, + /** 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 0000000000000..b71e15d34afe4 --- /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 0000000000000..b3f1eebb420b0 --- /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 { + 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 0000000000000..6fff440ec2fa2 --- /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() + val onClickedInvocations: List = _onClickedInvocations + + var onClickedResult: OnClickedResult = OnClickedResult.Handled + + private val _state = + MutableStateFlow( + KeyguardQuickAffordanceConfig.State.Hidden + ) + override val state: Flow = _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 0000000000000..a24fc93fedc25 --- /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>, +) : KeyguardQuickAffordanceConfigs { + + override fun getAll( + position: KeyguardQuickAffordancePosition + ): List { + return configsByPosition.getValue(position) + } + + override fun get( + configClass: KClass + ): 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 0000000000000..7d1cccb8a0a80 --- /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>() + + init { + KeyguardQuickAffordancePosition.values().forEach { value -> + modelByPosition[value] = MutableStateFlow(KeyguardQuickAffordanceModel.Hidden) + } + } + + override fun affordance( + position: KeyguardQuickAffordancePosition + ): Flow { + 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 0000000000000..c82803af4f377 --- /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 = + _animateBottomAreaDozingTransitions + + private val _bottomAreaAlpha = MutableStateFlow(1f) + override val bottomAreaAlpha: StateFlow = _bottomAreaAlpha + + private val _clockPosition = MutableStateFlow(Position(0, 0)) + override val clockPosition: StateFlow = _clockPosition + + private val _isDozing = + MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val isDozing: Flow = _isDozing + + private val _dozeAmount = + MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val dozeAmount: Flow = _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 0000000000000..bcc76abc89ba2 --- /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 + + 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() + 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 0000000000000..9ce572437b919 --- /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() + whenever(component.getControlsController()).thenReturn(Optional.of(controlsController)) + whenever(component.getControlsListingController()).thenReturn(Optional.empty()) + whenever(controlsController.getFavorites()).thenReturn(listOf(mock())) + + val values = mutableListOf() + 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 0000000000000..dc0e6f7663ff0 --- /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, + ): 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 0000000000000..2ee80349ff4cb --- /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() + 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() + val job = underTest.dozeAmount.onEach(values::add).launchIn(this) + + val captor = argumentCaptor() + 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 0000000000000..4bef00a13a073 --- /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() + 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() + 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() + 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() + 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 0000000000000..ee1d4d895e59d --- /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() + } +}