KeyguardBottomAreaView new impl. - data layer

- In this first CL in the series, we create the data layer that will power
the new implementation of KeyguardBottomAreaView
- On its own, this CL doesn't do anything and nothing is using it
- The data layer contains both the repositories and models
- There are two repositories:
  - The generic KeyguardRepository is responsible for providing generic
    application state that the view will ultimately consume. Note that
    some states are very specific to the bottom area and/or the view -
    this was done mostly as a shortcut because the current codebase
    makes it intractable to trace the true source of truth for these.
    Later, when things get cleaned up across the codebase, the
    repository can be changed to source the state from the real source
    of truth without making changes to its consumers
  - The specific KeyguardQuickAffordanceRepository is responsible for
    providing "quick affordance" (previously known as "bottom button")
    state. It creates a simple framework for defining the configuration
    for each of these buttons, positioning them on the left/right, and
    deciding which one takes precedence when more than on is available
    at the same position on the screen

Bug: 235403546
Test: Unit tests are included for both repositories and for two out of
the three quick affordance data sources. It was not possible to add a
unit test for the "home controls" data source because of my use of
`combine` which seems to not play nice with test coroutine scopes. I
also manually verified that the bottom area behaves as expected once the
entire CL chain is compiled together

Change-Id: I90f8ccf0233c8a089b564a831f14157bf2096c31
This commit is contained in:
Alejandro Nijamkin
2022-07-20 19:40:25 -07:00
parent c738a8ccae
commit 39fcf4f887
27 changed files with 2173 additions and 8 deletions

View File

@@ -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,
)
}
}
}

View File

@@ -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)
}

View File

@@ -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,
)

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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()
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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,
}

View File

@@ -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) }
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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()
}
}