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