Implementation of the FooterActions following the MAD (1/3)

This CL provides most of the implementation of the QS FooterActions
following the modern Android architecture. In particular, this CL
includes:
 - repositories in the data layer.
 - an interactor in the domain layer.
 - a view model in the ui layer.

The ViewBinder is added in ag/19674347, given that it comes together
with changes to the existing XML files.

Note that after this CL, the new implementation will still not be used
as it won't be wired yet. This is done in ag/19674347.

The highest value of the tests is in FooterActionsViewModelTest, which
focuses on testing the *state* (view model) of the footer actions using
the *real implementation* of the repositories, interactor and viewModel
all together. To do so, I implemented a FooterActionsUtils class that
allows to *easily* create real implementations of those repositories,
interactor and view model, without requiring the caller to provide any
parameter. I believe that this is even better than introducing new
fakes, and it should hopefully lead us towards using more and more of
the real implementations in our tests, and less and less of fakes/mocks,
making the tests much more useful. Of course, I still had to use fake &
mocks for the classes I'm calling to and for which instantiating the
actual object is too painful. As you can see, there are no test files
for the repositories: given that we already use the real implementations
in the ViewModel tests, they are defacto already tested.

I still added some tests on *interactions* (not *state*) in
FooterActionsInteractorTest. Even though I believe those tests don't
provide much value (they are merely a copy/paste of the implementation),
I preferred keeping the same coverage as the current tests (some of
which are going to be removed in ag/19674347).

Note that the business logic contained in this CL was mostly copy/pasted
from the current implementation, as I wanted to make sure that this is
going to be a pure refactoring that does not change the logic of this
feature. Still, I left some TODOs in the code for potential
improvements.

Bug: 242040009
Test: atest FooterActionsViewModelTest
Test: atest FooterActionsInteractorTest
Change-Id: Ia0bdf9824e098ad6604709f5db87576437b0a904
This commit is contained in:
Jordan Demeulenaere
2022-08-19 15:00:36 +02:00
parent c9a5847b55
commit 6fb22d4b16
23 changed files with 2016 additions and 12 deletions

View File

@@ -230,6 +230,7 @@ android_library {
libs: [ libs: [
"android.test.runner", "android.test.runner",
"android.test.base", "android.test.base",
"android.test.mock",
], ],
kotlincflags: ["-Xjvm-default=enable"], kotlincflags: ["-Xjvm-default=enable"],
aaptflags: [ aaptflags: [

View File

@@ -311,8 +311,7 @@ class ActivityLaunchAnimator(
@JvmStatic @JvmStatic
fun fromView(view: View, cujType: Int? = null): Controller? { fun fromView(view: View, cujType: Int? = null): Controller? {
if (view.parent !is ViewGroup) { if (view.parent !is ViewGroup) {
// TODO(b/192194319): Throw instead of just logging. Log.e(
Log.wtf(
TAG, TAG,
"Skipping animation as view $view is not attached to a ViewGroup", "Skipping animation as view $view is not attached to a ViewGroup",
Exception() Exception()

View File

@@ -0,0 +1,52 @@
/*
* 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.animation
import android.view.View
/** A piece of UI that can be expanded into a Dialog or an Activity. */
interface Expandable {
/**
* Create an [ActivityLaunchAnimator.Controller] that can be used to expand this [Expandable]
* into an Activity, or return `null` if this [Expandable] should not be animated (e.g. if it is
* currently not attached or visible).
*
* @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
* associated to the launch that will use this controller.
*/
fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
// TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
companion object {
/**
* Create an [Expandable] that will animate [view] when expanded.
*
* Note: The background of [view] should be a (rounded) rectangle so that it can be properly
* animated.
*/
fun fromView(view: View): Expandable {
return object : Expandable {
override fun activityLaunchController(
cujType: Int?,
): ActivityLaunchAnimator.Controller? {
return ActivityLaunchAnimator.Controller.fromView(view, cujType)
}
}
}
}
}

View File

@@ -54,6 +54,11 @@ android_library {
"testables", "testables",
"truth-prebuilt", "truth-prebuilt",
"androidx.test.uiautomator", "androidx.test.uiautomator",
"kotlinx_coroutines_test",
],
libs: [
"android.test.mock",
], ],
kotlincflags: ["-Xjvm-default=all"], kotlincflags: ["-Xjvm-default=all"],

View File

@@ -0,0 +1,33 @@
/*
* 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.shared.model
import android.annotation.StringRes
/**
* Models a content description, that can either be already [loaded][ContentDescription.Loaded] or
* be a [reference][ContentDescription.Resource] to a resource.
*/
sealed class ContentDescription {
data class Loaded(
val description: String?,
) : ContentDescription()
data class Resource(
@StringRes val res: Int,
) : ContentDescription()
}

View File

@@ -0,0 +1,32 @@
/*
* 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.ui
import android.widget.ImageView
import com.android.systemui.common.shared.model.Icon
object IconViewBinder {
fun bind(
icon: Icon,
view: ImageView,
) {
when (icon) {
is Icon.Loaded -> view.setImageDrawable(icon.drawable)
is Icon.Resource -> view.setImageResource(icon.res)
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.qs.footer.dagger
import com.android.systemui.qs.FgsManagerController
import com.android.systemui.qs.FgsManagerControllerImpl
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
import dagger.Binds
import dagger.Module
/** Dagger module to provide/bind footer actions singletons. */
@Module
interface FooterActionsModule {
@Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository
@Binds
fun foregroundServicesRepository(
impl: ForegroundServicesRepositoryImpl
): ForegroundServicesRepository
@Binds fun footerActionsInteractor(impl: FooterActionsInteractorImpl): FooterActionsInteractor
@Binds fun fgsManagerControllerImpl(impl: FgsManagerControllerImpl): FgsManagerController
}

View File

@@ -0,0 +1,32 @@
/*
* 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.qs.footer.data.model
import android.graphics.drawable.Drawable
/** The current status of the User Switcher. */
sealed class UserSwitcherStatusModel {
/** The user switcher is disabled. */
object Disabled : UserSwitcherStatusModel()
/** The user switcher is enabled. */
data class Enabled(
val currentUserName: String?,
val currentUserImage: Drawable?,
val isGuestUser: Boolean,
) : UserSwitcherStatusModel()
}

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.qs.footer.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.FgsManagerController
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
interface ForegroundServicesRepository {
/**
* The number of packages with a service running in the foreground.
*
* Note that this will be equal to 0 if [FgsManagerController.isAvailable] is false.
*/
val foregroundServicesCount: Flow<Int>
/**
* Whether there were new changes to the foreground packages since a dialog was last shown.
*
* Note that this will be equal to `false` if [FgsManagerController.showFooterDot] is false.
*/
val hasNewChanges: Flow<Boolean>
}
@SysUISingleton
class ForegroundServicesRepositoryImpl
@Inject
constructor(
fgsManagerController: FgsManagerController,
) : ForegroundServicesRepository {
override val foregroundServicesCount: Flow<Int> =
fgsManagerController.isAvailable
.flatMapLatest { isAvailable ->
if (!isAvailable) {
return@flatMapLatest flowOf(0)
}
conflatedCallbackFlow {
fun updateState(numberOfPackages: Int) {
trySendWithFailureLogging(numberOfPackages, TAG)
}
val listener =
object : FgsManagerController.OnNumberOfPackagesChangedListener {
override fun onNumberOfPackagesChanged(numberOfPackages: Int) {
updateState(numberOfPackages)
}
}
fgsManagerController.addOnNumberOfPackagesChangedListener(listener)
updateState(fgsManagerController.numRunningPackages)
awaitClose {
fgsManagerController.removeOnNumberOfPackagesChangedListener(listener)
}
}
}
.distinctUntilChanged()
override val hasNewChanges: Flow<Boolean> =
fgsManagerController.showFooterDot.flatMapLatest { showFooterDot ->
if (!showFooterDot) {
return@flatMapLatest flowOf(false)
}
// A flow that emits whenever the FGS dialog is dismissed.
val dialogDismissedEvents = conflatedCallbackFlow {
fun updateState() {
trySendWithFailureLogging(
Unit,
TAG,
)
}
val listener =
object : FgsManagerController.OnDialogDismissedListener {
override fun onDialogDismissed() {
updateState()
}
}
fgsManagerController.addOnDialogDismissedListener(listener)
awaitClose { fgsManagerController.removeOnDialogDismissedListener(listener) }
}
// Query [fgsManagerController.newChangesSinceDialogWasDismissed] everytime the dialog
// is dismissed or when [foregroundServices] is changing.
merge(
foregroundServicesCount,
dialogDismissedEvents,
)
.map { fgsManagerController.newChangesSinceDialogWasDismissed }
.distinctUntilChanged()
}
companion object {
private const val TAG = "ForegroundServicesRepositoryImpl"
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.qs.footer.data.repository
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.UserManager
import android.provider.Settings.Global.USER_SWITCHER_ENABLED
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.SettingObserver
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserInfoController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
interface UserSwitcherRepository {
/** The current [UserSwitcherStatusModel]. */
val userSwitcherStatus: Flow<UserSwitcherStatusModel>
}
@SysUISingleton
class UserSwitcherRepositoryImpl
@Inject
constructor(
@Application private val context: Context,
@Background private val bgHandler: Handler,
@Background private val bgDispatcher: CoroutineDispatcher,
private val userManager: UserManager,
private val userTracker: UserTracker,
private val userSwitcherController: UserSwitcherController,
private val userInfoController: UserInfoController,
private val globalSetting: GlobalSettings,
) : UserSwitcherRepository {
private val showUserSwitcherForSingleUser =
context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)
/** Whether the user switcher is currently enabled. */
private val isEnabled: Flow<Boolean> = conflatedCallbackFlow {
suspend fun updateState() {
trySendWithFailureLogging(isUserSwitcherEnabled(), TAG)
}
val observer =
object :
SettingObserver(
globalSetting,
bgHandler,
USER_SWITCHER_ENABLED,
userTracker.userId,
) {
override fun handleValueChanged(value: Int, observedChange: Boolean) {
if (observedChange) {
launch { updateState() }
}
}
}
observer.isListening = true
updateState()
awaitClose { observer.isListening = false }
}
/** The current user name. */
private val currentUserName: Flow<String?> = conflatedCallbackFlow {
suspend fun updateState() {
trySendWithFailureLogging(getCurrentUser(), TAG)
}
val callback = UserSwitcherController.UserSwitchCallback { launch { updateState() } }
userSwitcherController.addUserSwitchCallback(callback)
updateState()
awaitClose { userSwitcherController.removeUserSwitchCallback(callback) }
}
/** The current (icon, isGuestUser) values. */
// TODO(b/242040009): Could we only use this callback to get the user name and remove
// currentUsername above?
private val currentUserInfo: Flow<Pair<Drawable?, Boolean>> = conflatedCallbackFlow {
val listener =
UserInfoController.OnUserInfoChangedListener { _, picture, _ ->
launch { trySendWithFailureLogging(picture to isGuestUser(), TAG) }
}
// This will automatically call the listener when attached, so no need to update the state
// here.
userInfoController.addCallback(listener)
awaitClose { userInfoController.removeCallback(listener) }
}
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
isEnabled
.flatMapLatest { enabled ->
if (enabled) {
combine(currentUserName, currentUserInfo) { name, (icon, isGuest) ->
UserSwitcherStatusModel.Enabled(name, icon, isGuest)
}
} else {
flowOf(UserSwitcherStatusModel.Disabled)
}
}
.distinctUntilChanged()
private suspend fun isUserSwitcherEnabled(): Boolean {
return withContext(bgDispatcher) {
userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser)
}
}
private suspend fun getCurrentUser(): String? {
return withContext(bgDispatcher) { userSwitcherController.currentUserName }
}
private suspend fun isGuestUser(): Boolean {
return withContext(bgDispatcher) {
userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser())
}
}
companion object {
private const val TAG = "UserSwitcherRepositoryImpl"
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.qs.footer.domain.interactor
import android.app.admin.DevicePolicyEventLogger
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import android.provider.Settings
import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.UiEventLogger
import com.android.internal.logging.nano.MetricsProto
import com.android.internal.util.FrameworkStatsLog
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.FgsManagerController
import com.android.systemui.qs.QSSecurityFooterUtils
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.security.data.repository.SecurityRepository
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.UserSwitcherActivity
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
/** Interactor for the footer actions business logic. */
interface FooterActionsInteractor {
/** The current [SecurityButtonConfig]. */
val securityButtonConfig: Flow<SecurityButtonConfig?>
/** The number of packages with a service running in the foreground. */
val foregroundServicesCount: Flow<Int>
/** Whether there are new packages with a service running in the foreground. */
val hasNewForegroundServices: Flow<Boolean>
/** The current [UserSwitcherStatusModel]. */
val userSwitcherStatus: Flow<UserSwitcherStatusModel>
/**
* The flow emitting `Unit` whenever a request to show the device monitoring dialog is fired.
*/
val deviceMonitoringDialogRequests: Flow<Unit>
/**
* Show the device monitoring dialog, expanded from [view].
*
* Important: [view] must be associated to the same [Context] as the [Quick Settings fragment]
* [com.android.systemui.qs.QSFragment].
*/
// TODO(b/230830644): Replace view by Expandable interface.
fun showDeviceMonitoringDialog(view: View)
/**
* Show the device monitoring dialog.
*
* Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings
* fragment][com.android.systemui.qs.QSFragment].
*/
// TODO(b/230830644): Replace view by Expandable interface.
fun showDeviceMonitoringDialog(quickSettingsContext: Context)
/** Show the foreground services dialog. */
// TODO(b/230830644): Replace view by Expandable interface.
fun showForegroundServicesDialog(view: View)
/** Show the power menu dialog. */
// TODO(b/230830644): Replace view by Expandable interface.
fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View)
/** Show the settings. */
fun showSettings(expandable: Expandable)
/** Show the user switcher. */
// TODO(b/230830644): Replace view by Expandable interface.
fun showUserSwitcher(view: View)
}
@SysUISingleton
class FooterActionsInteractorImpl
@Inject
constructor(
private val activityStarter: ActivityStarter,
private val featureFlags: FeatureFlags,
private val metricsLogger: MetricsLogger,
private val uiEventLogger: UiEventLogger,
private val deviceProvisionedController: DeviceProvisionedController,
private val qsSecurityFooterUtils: QSSecurityFooterUtils,
private val fgsManagerController: FgsManagerController,
private val userSwitchDialogController: UserSwitchDialogController,
securityRepository: SecurityRepository,
foregroundServicesRepository: ForegroundServicesRepository,
userSwitcherRepository: UserSwitcherRepository,
broadcastDispatcher: BroadcastDispatcher,
@Background bgDispatcher: CoroutineDispatcher,
) : FooterActionsInteractor {
override val securityButtonConfig: Flow<SecurityButtonConfig?> =
securityRepository.security.map { security ->
withContext(bgDispatcher) { qsSecurityFooterUtils.getButtonConfig(security) }
}
override val foregroundServicesCount: Flow<Int> =
foregroundServicesRepository.foregroundServicesCount
override val hasNewForegroundServices: Flow<Boolean> =
foregroundServicesRepository.hasNewChanges
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
userSwitcherRepository.userSwitcherStatus
override val deviceMonitoringDialogRequests: Flow<Unit> =
broadcastDispatcher.broadcastFlow(
IntentFilter(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG),
UserHandle.ALL,
Context.RECEIVER_EXPORTED,
null,
)
override fun showDeviceMonitoringDialog(view: View) {
qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view)
DevicePolicyEventLogger.createEvent(
FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
)
.write()
}
override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
}
override fun showForegroundServicesDialog(view: View) {
fgsManagerController.showDialog(view)
}
override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
globalActionsDialogLite.showOrHideDialog(
/* keyguardShowing= */ false,
/* isDeviceProvisioned= */ true,
view,
)
}
override fun showSettings(expandable: Expandable) {
if (!deviceProvisionedController.isCurrentUserSetup) {
// If user isn't setup just unlock the device and dump them back at SUW.
activityStarter.postQSRunnableDismissingKeyguard {}
return
}
metricsLogger.action(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH)
activityStarter.startActivity(
Intent(Settings.ACTION_SETTINGS),
true /* dismissShade */,
expandable.activityLaunchController(
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON
),
)
}
override fun showUserSwitcher(view: View) {
if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
userSwitchDialogController.showDialog(view)
return
}
val intent =
Intent(view.context, UserSwitcherActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
}
activityStarter.startActivity(
intent,
true /* dismissShade */,
ActivityLaunchAnimator.Controller.fromView(view, null),
true /* showOverlockscreenwhenlocked */,
UserHandle.SYSTEM,
)
}
}

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.qs.footer.ui.viewmodel
import android.annotation.DrawableRes
import android.view.View
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
/**
* A ViewModel for a simple footer actions button. This is used for the user switcher, settings and
* power buttons.
*/
data class FooterActionsButtonViewModel(
val icon: Icon,
val iconTint: Int?,
@DrawableRes val background: Int,
val contentDescription: ContentDescription,
// TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
// or activity.
val onClick: (View) -> Unit,
)

View File

@@ -0,0 +1,28 @@
/*
* 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.qs.footer.ui.viewmodel
import android.view.View
/** A ViewModel for the foreground services button. */
data class FooterActionsForegroundServicesButtonViewModel(
val foregroundServicesCount: Int,
val text: String,
val displayText: Boolean,
val hasNewChanges: Boolean,
val onClick: (View) -> Unit,
)

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.qs.footer.ui.viewmodel
import android.view.View
import com.android.systemui.common.shared.model.Icon
/** A ViewModel for the security button. */
data class FooterActionsSecurityButtonViewModel(
val icon: Icon,
val text: String,
val onClick: ((View) -> Unit)?,
)

View File

@@ -0,0 +1,304 @@
/*
* 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.qs.footer.ui.viewmodel
import android.content.Context
import android.util.Log
import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.android.settingslib.Utils
import com.android.settingslib.drawable.UserIconDrawable
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
import com.android.systemui.util.icuMessageFormat
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import kotlin.math.max
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
/** A ViewModel for the footer actions. */
class FooterActionsViewModel(
@Application private val context: Context,
private val footerActionsInteractor: FooterActionsInteractor,
private val falsingManager: FalsingManager,
private val globalActionsDialogLite: GlobalActionsDialogLite,
showPowerButton: Boolean,
) {
/**
* Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
* the UI should still participate to the layout it is included in (i.e. in the View world it
* should be INVISIBLE, not GONE).
*/
private val _isVisible = MutableStateFlow(true)
val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow()
/** The alpha the UI rendering this ViewModel should have. */
private val _alpha = MutableStateFlow(1f)
val alpha: StateFlow<Float> = _alpha.asStateFlow()
/** The alpha the background of the UI rendering this ViewModel should have. */
private val _backgroundAlpha = MutableStateFlow(1f)
val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow()
/** The model for the security button. */
val security: Flow<FooterActionsSecurityButtonViewModel?> =
footerActionsInteractor.securityButtonConfig.map { config ->
val (icon, text, isClickable) = config ?: return@map null
FooterActionsSecurityButtonViewModel(
icon,
text,
if (isClickable) this::onSecurityButtonClicked else null,
)
}
/** The model for the foreground services button. */
val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?> =
combine(
footerActionsInteractor.foregroundServicesCount,
footerActionsInteractor.hasNewForegroundServices,
security,
) { foregroundServicesCount, hasNewChanges, securityModel ->
if (foregroundServicesCount <= 0) {
return@combine null
}
val text =
icuMessageFormat(
context.resources,
R.string.fgs_manager_footer_label,
foregroundServicesCount,
)
FooterActionsForegroundServicesButtonViewModel(
foregroundServicesCount,
text = text,
displayText = securityModel == null,
hasNewChanges = hasNewChanges,
this::onForegroundServiceButtonClicked,
)
}
/** The model for the user switcher button. */
val userSwitcher: Flow<FooterActionsButtonViewModel?> =
footerActionsInteractor.userSwitcherStatus.map { userSwitcherStatus ->
when (userSwitcherStatus) {
UserSwitcherStatusModel.Disabled -> null
is UserSwitcherStatusModel.Enabled -> {
if (userSwitcherStatus.currentUserImage == null) {
Log.e(
TAG,
"Skipped the addition of user switcher button because " +
"currentUserImage is missing",
)
return@map null
}
userSwitcherButton(userSwitcherStatus)
}
}
}
/** The model for the settings button. */
val settings: FooterActionsButtonViewModel =
FooterActionsButtonViewModel(
Icon.Resource(R.drawable.ic_settings),
iconTint = null,
R.drawable.qs_footer_action_circle,
ContentDescription.Resource(R.string.accessibility_quick_settings_settings),
this::onSettingsButtonClicked,
)
/** The model for the power button. */
val power: FooterActionsButtonViewModel? =
if (showPowerButton) {
FooterActionsButtonViewModel(
Icon.Resource(android.R.drawable.ic_lock_power_off),
iconTint =
Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.textColorOnAccent,
),
R.drawable.qs_footer_action_circle_color,
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu),
this::onPowerButtonClicked,
)
} else {
null
}
/** Called when the visibility of the UI rendering this model should be changed. */
fun onVisibilityChangeRequested(visible: Boolean) {
_isVisible.value = visible
}
/** Called when the expansion of the Quick Settings changed. */
fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
if (isInSplitShade) {
// In split shade, we want to fade in the background only at the very end (see
// b/240563302).
val delay = 0.99f
_alpha.value = expansion
_backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay)
} else {
// Only start fading in the footer actions when we are at least 90% expanded.
val delay = 0.9f
_alpha.value = max(0f, expansion - delay) / (1 - delay)
_backgroundAlpha.value = 1f
}
}
/**
* Observe the device monitoring dialog requests and show the dialog accordingly. This function
* will suspend indefinitely and will need to be cancelled to stop observing.
*
* Important: [quickSettingsContext] must be the [Context] associated to the [Quick Settings
* fragment][com.android.systemui.qs.QSFragment], and the call to this function must be
* cancelled when that fragment is destroyed.
*/
suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
footerActionsInteractor.deviceMonitoringDialogRequests.collect {
footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext)
}
}
private fun onSecurityButtonClicked(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
footerActionsInteractor.showDeviceMonitoringDialog(view)
}
private fun onForegroundServiceButtonClicked(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
footerActionsInteractor.showForegroundServicesDialog(view)
}
private fun onUserSwitcherClicked(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
footerActionsInteractor.showUserSwitcher(view)
}
// TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
// or activity.
private fun onSettingsButtonClicked(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
footerActionsInteractor.showSettings(Expandable.fromView(view))
}
private fun onPowerButtonClicked(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view)
}
private fun userSwitcherButton(
status: UserSwitcherStatusModel.Enabled
): FooterActionsButtonViewModel {
val icon = status.currentUserImage!!
val iconTint =
if (status.isGuestUser && icon !is UserIconDrawable) {
Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground)
} else {
null
}
return FooterActionsButtonViewModel(
Icon.Loaded(icon),
iconTint,
R.drawable.qs_footer_action_circle,
ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
this::onUserSwitcherClicked,
)
}
private fun userSwitcherContentDescription(currentUser: String?): String? {
return currentUser?.let { user ->
context.getString(R.string.accessibility_quick_settings_user, user)
}
}
@SysUISingleton
class Factory
@Inject
constructor(
@Application private val context: Context,
private val falsingManager: FalsingManager,
private val footerActionsInteractor: FooterActionsInteractor,
private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
@Named(PM_LITE_ENABLED) private val showPowerButton: Boolean,
) {
/** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */
fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
// This should usually not happen, but let's make sure we already destroy
// globalActionsDialogLite.
globalActionsDialogLite.destroy()
} else {
// Destroy globalActionsDialogLite when the lifecycle is destroyed.
lifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
globalActionsDialogLite.destroy()
}
}
)
}
return FooterActionsViewModel(
context,
footerActionsInteractor,
falsingManager,
globalActionsDialogLite,
showPowerButton,
)
}
}
companion object {
private const val TAG = "FooterActionsViewModel"
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.security.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.security.data.model.SecurityModel
import com.android.systemui.statusbar.policy.SecurityController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
interface SecurityRepository {
/** The current [SecurityModel]. */
val security: Flow<SecurityModel>
}
@SysUISingleton
class SecurityRepositoryImpl
@Inject
constructor(
private val securityController: SecurityController,
@Background private val bgDispatcher: CoroutineDispatcher,
) : SecurityRepository {
override val security: Flow<SecurityModel> = conflatedCallbackFlow {
suspend fun updateState() {
trySendWithFailureLogging(SecurityModel.create(securityController, bgDispatcher), TAG)
}
val callback = SecurityController.SecurityControllerCallback { launch { updateState() } }
securityController.addCallback(callback)
updateState()
awaitClose { securityController.removeCallback(callback) }
}
companion object {
private const val TAG = "SecurityRepositoryImpl"
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.security.data.repository
import dagger.Binds
import dagger.Module
/** Dagger module to provide/bind security repositories. */
@Module
interface SecurityRepositoryModule {
@Binds fun securityRepository(impl: SecurityRepositoryImpl): SecurityRepository
}

View File

@@ -10,12 +10,10 @@ import android.graphics.Rect
import android.os.Looper import android.os.Looper
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper import android.testing.TestableLooper.RunWithLooper
import android.util.Log
import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationFinishedCallback
import android.view.RemoteAnimationAdapter import android.view.RemoteAnimationAdapter
import android.view.RemoteAnimationTarget import android.view.RemoteAnimationTarget
import android.view.SurfaceControl import android.view.SurfaceControl
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
@@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Mock lateinit var listener: ActivityLaunchAnimator.Listener @Mock lateinit var listener: ActivityLaunchAnimator.Listener
@Spy private val controller = TestLaunchAnimatorController(launchContainer) @Spy private val controller = TestLaunchAnimatorController(launchContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
@Mock lateinit var failHandler: Log.TerribleFailureHandler
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@get:Rule val rule = MockitoJUnit.rule() @get:Rule val rule = MockitoJUnit.rule()
@@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
verify(controller).onLaunchAnimationStart(anyBoolean()) verify(controller).onLaunchAnimationStart(anyBoolean())
} }
@Test
fun controllerFromOrphanViewReturnsNullAndIsATerribleFailure() {
Log.setWtfHandler(failHandler)
assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext)))
verify(failHandler).onTerribleFailure(any(), any(), anyBoolean())
}
private fun fakeWindow(): RemoteAnimationTarget { private fun fakeWindow(): RemoteAnimationTarget {
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */) val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
val taskInfo = ActivityManager.RunningTaskInfo() val taskInfo = ActivityManager.RunningTaskInfo()

View File

@@ -0,0 +1,212 @@
/*
* 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.qs.footer.domain.interactor
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.UserHandle
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.logging.nano.MetricsProto
import com.android.internal.logging.testing.FakeMetricsLogger
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.QSSecurityFooterUtils
import com.android.systemui.qs.footer.FooterActionsTestUtils
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.truth.correspondence.FakeUiEvent
import com.android.systemui.truth.correspondence.LogMaker
import com.android.systemui.user.UserSwitcherActivity
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class FooterActionsInteractorTest : SysuiTestCase() {
private lateinit var utils: FooterActionsTestUtils
@Before
fun setUp() {
utils = FooterActionsTestUtils(context, TestableLooper.get(this))
}
@Test
fun showDeviceMonitoringDialog() {
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
val quickSettingsContext = mock<Context>()
underTest.showDeviceMonitoringDialog(quickSettingsContext)
verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
val view = mock<View>()
whenever(view.context).thenReturn(quickSettingsContext)
underTest.showDeviceMonitoringDialog(view)
verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
}
@Test
fun showPowerMenuDialog() {
val uiEventLogger = UiEventLoggerFake()
val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger)
val globalActionsDialogLite = mock<GlobalActionsDialogLite>()
val view = mock<View>()
underTest.showPowerMenuDialog(globalActionsDialogLite, view)
// Event is logged.
val logs = uiEventLogger.logs
assertThat(logs)
.comparingElementsUsing(FakeUiEvent.EVENT_ID)
.containsExactly(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS.id)
// Dialog is shown.
verify(globalActionsDialogLite)
.showOrHideDialog(
/* keyguardShowing= */ false,
/* isDeviceProvisioned= */ true,
view,
)
}
@Test
fun showSettings_userSetUp() {
val activityStarter = mock<ActivityStarter>()
val deviceProvisionedController = mock<DeviceProvisionedController>()
val metricsLogger = FakeMetricsLogger()
// User is set up.
whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
val underTest =
utils.footerActionsInteractor(
activityStarter = activityStarter,
deviceProvisionedController = deviceProvisionedController,
metricsLogger = metricsLogger,
)
underTest.showSettings(mock())
// Event is logged.
assertThat(metricsLogger.logs.toList())
.comparingElementsUsing(LogMaker.CATEGORY)
.containsExactly(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH)
// Activity is started.
val intentCaptor = argumentCaptor<Intent>()
verify(activityStarter)
.startActivity(
intentCaptor.capture(),
/* dismissShade= */ eq(true),
nullable() as? ActivityLaunchAnimator.Controller,
)
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SETTINGS)
}
@Test
fun showSettings_userNotSetUp() {
val activityStarter = mock<ActivityStarter>()
val deviceProvisionedController = mock<DeviceProvisionedController>()
// User is not set up.
whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
val underTest =
utils.footerActionsInteractor(
activityStarter = activityStarter,
deviceProvisionedController = deviceProvisionedController,
)
underTest.showSettings(mock())
// We only unlock the device.
verify(activityStarter).postQSRunnableDismissingKeyguard(any())
}
@Test
fun showUserSwitcher_fullScreenDisabled() {
val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
val userSwitchDialogController = mock<UserSwitchDialogController>()
val underTest =
utils.footerActionsInteractor(
featureFlags = featureFlags,
userSwitchDialogController = userSwitchDialogController,
)
val view = mock<View>()
underTest.showUserSwitcher(view)
// Dialog is shown.
verify(userSwitchDialogController).showDialog(view)
}
@Test
fun showUserSwitcher_fullScreenEnabled() {
val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
val activityStarter = mock<ActivityStarter>()
val underTest =
utils.footerActionsInteractor(
featureFlags = featureFlags,
activityStarter = activityStarter,
)
// The clicked view. The context is necessary because it's used to build the intent, that
// we check below.
val view = mock<View>()
whenever(view.context).thenReturn(context)
underTest.showUserSwitcher(view)
// Dialog is shown.
val intentCaptor = argumentCaptor<Intent>()
verify(activityStarter)
.startActivity(
intentCaptor.capture(),
/* dismissShade= */ eq(true),
/* ActivityLaunchAnimator.Controller= */ nullable(),
/* showOverLockscreenWhenLocked= */ eq(true),
eq(UserHandle.SYSTEM),
)
assertThat(intentCaptor.value.component)
.isEqualTo(
ComponentName(
context,
UserSwitcherActivity::class.java,
)
)
}
}

View File

@@ -0,0 +1,408 @@
/*
* 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.qs.footer.ui.viewmodel
import android.graphics.drawable.Drawable
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.settingslib.Utils
import com.android.settingslib.drawable.UserIconDrawable
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.FakeFgsManagerController
import com.android.systemui.qs.QSSecurityFooterUtils
import com.android.systemui.qs.footer.FooterActionsTestUtils
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
import com.android.systemui.security.data.model.SecurityModel
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.FakeSecurityController
import com.android.systemui.statusbar.policy.FakeUserInfoController
import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo
import com.android.systemui.statusbar.policy.MockUserSwitcherControllerWrapper
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class FooterActionsViewModelTest : SysuiTestCase() {
private lateinit var utils: FooterActionsTestUtils
@Before
fun setUp() {
utils = FooterActionsTestUtils(context, TestableLooper.get(this))
}
@Test
fun settingsButton() = runBlockingTest {
val underTest = utils.footerActionsViewModel(showPowerButton = false)
val settings = underTest.settings
assertThat(settings.contentDescription)
.isEqualTo(ContentDescription.Resource(R.string.accessibility_quick_settings_settings))
assertThat(settings.icon).isEqualTo(Icon.Resource(R.drawable.ic_settings))
assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
assertThat(settings.iconTint).isNull()
}
@Test
fun powerButton() = runBlockingTest {
// Without power button.
val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false)
assertThat(underTestWithoutPower.power).isNull()
// With power button.
val underTestWithPower = utils.footerActionsViewModel(showPowerButton = true)
val power = underTestWithPower.power
assertThat(power).isNotNull()
assertThat(power!!.contentDescription)
.isEqualTo(
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
)
assertThat(power.icon).isEqualTo(Icon.Resource(android.R.drawable.ic_lock_power_off))
assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
assertThat(power.iconTint)
.isEqualTo(
Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.textColorOnAccent,
),
)
}
@Test
fun userSwitcher() = runBlockingTest {
val picture: Drawable = mock()
val userInfoController = FakeUserInfoController(FakeInfo(picture = picture))
val settings = FakeSettings()
val userId = 42
val userTracker = FakeUserTracker(userId)
val userSwitcherControllerWrapper =
MockUserSwitcherControllerWrapper(currentUserName = "foo")
// Mock UserManager.
val userManager = mock<UserManager>()
var isUserSwitcherEnabled = false
var isGuestUser = false
whenever(userManager.isUserSwitcherEnabled(any())).thenAnswer { isUserSwitcherEnabled }
whenever(userManager.isGuestUser(any())).thenAnswer { isGuestUser }
val underTest =
utils.footerActionsViewModel(
showPowerButton = false,
footerActionsInteractor =
utils.footerActionsInteractor(
userSwitcherRepository =
utils.userSwitcherRepository(
userTracker = userTracker,
settings = settings,
userManager = userManager,
userInfoController = userInfoController,
userSwitcherController = userSwitcherControllerWrapper.controller,
),
)
)
// Collect the user switcher into currentUserSwitcher.
var currentUserSwitcher: FooterActionsButtonViewModel? = null
val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } }
fun currentUserSwitcher(): FooterActionsButtonViewModel? {
// Make sure we finish collecting the current user switcher. This is necessary because
// combined flows launch multiple coroutines in the current scope so we need to make
// sure we process all coroutines triggered by our flow collection before we make
// assertions on the current buttons.
advanceUntilIdle()
return currentUserSwitcher
}
// The user switcher is disabled.
assertThat(currentUserSwitcher()).isNull()
// Make the user manager return that the User Switcher is enabled. A change of the setting
// for the current user will be fired to notify us of that change.
isUserSwitcherEnabled = true
// Update the setting for a random user: nothing should change, given that at this point we
// weren't notified of the change yet.
utils.setUserSwitcherEnabled(settings, true, 3)
assertThat(currentUserSwitcher()).isNull()
// Update the setting for the observed user: now we will be notified and the button should
// be there.
utils.setUserSwitcherEnabled(settings, true, userId)
val userSwitcher = currentUserSwitcher()
assertThat(userSwitcher).isNotNull()
assertThat(userSwitcher!!.contentDescription)
.isEqualTo(ContentDescription.Loaded("Signed in as foo"))
assertThat(userSwitcher.icon).isEqualTo(Icon.Loaded(picture))
assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
// Change the current user name.
userSwitcherControllerWrapper.currentUserName = "bar"
assertThat(currentUserSwitcher()?.contentDescription)
.isEqualTo(ContentDescription.Loaded("Signed in as bar"))
fun iconTint(): Int? = currentUserSwitcher()!!.iconTint
// We tint the icon if the current user is not the guest.
assertThat(iconTint()).isNull()
// Make the UserManager return that the current user is the guest. A change of the user
// info will be fired to notify us of that change.
isGuestUser = true
// At this point, there was no change of the user info yet so we still didn't pick the
// UserManager change.
assertThat(iconTint()).isNull()
// Trigger a user info change: there should now be a tint.
userInfoController.updateInfo { userAccount = "doe" }
assertThat(iconTint())
.isEqualTo(
Utils.getColorAttrDefaultColor(
context,
android.R.attr.colorForeground,
)
)
// Make sure we don't tint the icon if it is a user image (and not the default image), even
// in guest mode.
userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() }
assertThat(iconTint()).isNull()
job.cancel()
}
@Test
fun security() = runBlockingTest {
val securityController = FakeSecurityController()
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
// logic in securityToConfig.
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
securityToConfig(it.arguments.first() as SecurityModel)
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
securityRepository =
utils.securityRepository(
securityController = securityController,
),
),
)
// Collect the security model into currentSecurity.
var currentSecurity: FooterActionsSecurityButtonViewModel? = null
val job = launch { underTest.security.collect { currentSecurity = it } }
fun currentSecurity(): FooterActionsSecurityButtonViewModel? {
advanceUntilIdle()
return currentSecurity
}
// By default, we always return a null SecurityButtonConfig.
assertThat(currentSecurity()).isNull()
// Map any SecurityModel into a non-null SecurityButtonConfig.
val buttonConfig =
SecurityButtonConfig(
icon = Icon.Resource(0),
text = "foo",
isClickable = true,
)
securityToConfig = { buttonConfig }
// There was no change of the security info yet, so the mapper was not called yet.
assertThat(currentSecurity()).isNull()
// Trigger a SecurityModel change, which will call the mapper and add a button.
securityController.updateState {}
var security = currentSecurity()
assertThat(security).isNotNull()
assertThat(security!!.icon).isEqualTo(buttonConfig.icon)
assertThat(security.text).isEqualTo(buttonConfig.text)
assertThat(security.onClick).isNotNull()
// If the config.clickable = false, then onClick should be null.
securityToConfig = { buttonConfig.copy(isClickable = false) }
securityController.updateState {}
security = currentSecurity()
assertThat(security).isNotNull()
assertThat(security!!.onClick).isNull()
job.cancel()
}
@Test
fun foregroundServices() = runBlockingTest {
val securityController = FakeSecurityController()
val fgsManagerController =
FakeFgsManagerController(
isAvailable = true,
showFooterDot = false,
numRunningPackages = 0,
)
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
// logic in securityToConfig.
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
securityToConfig(it.arguments.first() as SecurityModel)
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
securityRepository = utils.securityRepository(securityController),
foregroundServicesRepository =
utils.foregroundServicesRepository(fgsManagerController),
),
)
// Collect the security model into currentSecurity.
var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } }
fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? {
advanceUntilIdle()
return currentForegroundServices
}
// We don't show the foreground services button if the number of running packages is not
// > 1.
assertThat(currentForegroundServices()).isNull()
// We show it at soon as the number of services is at least 1. Given that there is no
// security, it should be displayed with text.
fgsManagerController.numRunningPackages = 1
val foregroundServices = currentForegroundServices()
assertThat(foregroundServices).isNotNull()
assertThat(foregroundServices!!.foregroundServicesCount).isEqualTo(1)
assertThat(foregroundServices.text).isEqualTo("1 app is active")
assertThat(foregroundServices.displayText).isTrue()
assertThat(foregroundServices.onClick).isNotNull()
// We handle plurals correctly.
fgsManagerController.numRunningPackages = 3
assertThat(currentForegroundServices()?.text).isEqualTo("3 apps are active")
// Showing new changes (the footer dot) is currently disabled.
assertThat(foregroundServices.hasNewChanges).isFalse()
// Enabling it will show the new changes.
fgsManagerController.showFooterDot.value = true
assertThat(currentForegroundServices()?.hasNewChanges).isTrue()
// Dismissing the dialog should remove the new changes dot.
fgsManagerController.simulateDialogDismiss()
assertThat(currentForegroundServices()?.hasNewChanges).isFalse()
// Showing the security button will make this show as a simple button without text.
assertThat(foregroundServices.displayText).isTrue()
securityToConfig = {
SecurityButtonConfig(
icon = Icon.Resource(0),
text = "foo",
isClickable = true,
)
}
securityController.updateState {}
assertThat(currentForegroundServices()?.displayText).isFalse()
job.cancel()
}
@Test
fun observeDeviceMonitoringDialogRequests() = runBlockingTest {
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
val broadcastDispatcher = mock<BroadcastDispatcher>()
// Return a fake broadcastFlow that emits 3 fake events when collected.
val broadcastFlow = flowOf(Unit, Unit, Unit)
whenever(
broadcastDispatcher.broadcastFlow(
any(),
nullable(),
anyInt(),
nullable(),
)
)
.thenAnswer { broadcastFlow }
// Increment nDialogRequests whenever a request to show the dialog is made by the
// FooterActionsInteractor.
var nDialogRequests = 0
whenever(qsSecurityFooterUtils.showDeviceMonitoringDialog(any(), nullable())).then {
nDialogRequests++
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
broadcastDispatcher = broadcastDispatcher,
),
)
val job = launch {
underTest.observeDeviceMonitoringDialogRequests(quickSettingsContext = mock())
}
advanceUntilIdle()
assertThat(nDialogRequests).isEqualTo(3)
job.cancel()
}
@Test
fun isVisible() {
val underTest = utils.footerActionsViewModel()
assertThat(underTest.isVisible.value).isTrue()
underTest.onVisibilityChangeRequested(visible = false)
assertThat(underTest.isVisible.value).isFalse()
underTest.onVisibilityChangeRequested(visible = true)
assertThat(underTest.isVisible.value).isTrue()
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.qs.footer
import android.content.Context
import android.os.Handler
import android.os.UserManager
import android.provider.Settings
import android.testing.TestableLooper
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.UiEventLogger
import com.android.internal.logging.testing.FakeMetricsLogger
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.FakeFgsManagerController
import com.android.systemui.qs.FgsManagerController
import com.android.systemui.qs.QSSecurityFooterUtils
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.security.data.repository.SecurityRepository
import com.android.systemui.security.data.repository.SecurityRepositoryImpl
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.FakeSecurityController
import com.android.systemui.statusbar.policy.FakeUserInfoController
import com.android.systemui.statusbar.policy.SecurityController
import com.android.systemui.statusbar.policy.UserInfoController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineDispatcher
/**
* Util class to create real implementations of the FooterActions repositories, viewModel and
* interactor to be used in tests.
*/
class FooterActionsTestUtils(
private val context: Context,
private val testableLooper: TestableLooper,
private val fakeClock: FakeSystemClock = FakeSystemClock(),
) {
/** Enable or disable the user switcher in the settings. */
fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) {
settings.putBoolForUser(Settings.Global.USER_SWITCHER_ENABLED, enabled, userId)
// The settings listener is processing messages on the bgHandler (usually backed by a
// testableLooper in tests), so let's make sure we process the callback before continuing.
testableLooper.processAllMessages()
}
/** Create a [FooterActionsViewModel] to be used in tests. */
fun footerActionsViewModel(
@Application context: Context = this.context.applicationContext,
footerActionsInteractor: FooterActionsInteractor = footerActionsInteractor(),
falsingManager: FalsingManager = FalsingManagerFake(),
globalActionsDialogLite: GlobalActionsDialogLite = mock(),
showPowerButton: Boolean = true,
): FooterActionsViewModel {
return FooterActionsViewModel(
context,
footerActionsInteractor,
falsingManager,
globalActionsDialogLite,
showPowerButton,
)
}
/** Create a [FooterActionsInteractor] to be used in tests. */
fun footerActionsInteractor(
activityStarter: ActivityStarter = mock(),
featureFlags: FeatureFlags = FakeFeatureFlags(),
metricsLogger: MetricsLogger = FakeMetricsLogger(),
uiEventLogger: UiEventLogger = UiEventLoggerFake(),
deviceProvisionedController: DeviceProvisionedController = mock(),
qsSecurityFooterUtils: QSSecurityFooterUtils = mock(),
fgsManagerController: FgsManagerController = mock(),
userSwitchDialogController: UserSwitchDialogController = mock(),
securityRepository: SecurityRepository = securityRepository(),
foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(),
userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(),
broadcastDispatcher: BroadcastDispatcher = mock(),
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
): FooterActionsInteractor {
return FooterActionsInteractorImpl(
activityStarter,
featureFlags,
metricsLogger,
uiEventLogger,
deviceProvisionedController,
qsSecurityFooterUtils,
fgsManagerController,
userSwitchDialogController,
securityRepository,
foregroundServicesRepository,
userSwitcherRepository,
broadcastDispatcher,
bgDispatcher,
)
}
/** Create a [SecurityRepository] to be used in tests. */
fun securityRepository(
securityController: SecurityController = FakeSecurityController(),
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
): SecurityRepository {
return SecurityRepositoryImpl(
securityController,
bgDispatcher,
)
}
/** Create a [SecurityRepository] to be used in tests. */
fun foregroundServicesRepository(
fgsManagerController: FakeFgsManagerController = FakeFgsManagerController(),
): ForegroundServicesRepository {
return ForegroundServicesRepositoryImpl(fgsManagerController)
}
/** Create a [UserSwitcherRepository] to be used in tests. */
fun userSwitcherRepository(
@Application context: Context = this.context.applicationContext,
bgHandler: Handler = Handler(testableLooper.looper),
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
userManager: UserManager = mock(),
userTracker: UserTracker = FakeUserTracker(),
userSwitcherController: UserSwitcherController = mock(),
userInfoController: UserInfoController = FakeUserInfoController(),
settings: GlobalSettings = FakeSettings(),
): UserSwitcherRepository {
return UserSwitcherRepositoryImpl(
context,
bgHandler,
bgDispatcher,
userManager,
userTracker,
userSwitcherController,
userInfoController,
settings,
)
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.truth.correspondence
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.logging.testing.UiEventLoggerFake.FakeUiEvent
import com.google.common.truth.Correspondence
/** Instances of [Correspondence] to match a [UiEventLoggerFake.FakeUiEvent] with Truth. */
object FakeUiEvent {
val EVENT_ID =
Correspondence.transforming<FakeUiEvent, Int>(
{ it?.eventId },
"has a eventId of",
)
}

View File

@@ -0,0 +1,29 @@
/*
* 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.truth.correspondence
import android.metrics.LogMaker
import com.google.common.truth.Correspondence
/** Instances of [Correspondence] to match a [LogMaker] with Truth. */
object LogMaker {
val CATEGORY =
Correspondence.transforming<LogMaker, Int>(
{ it?.category },
"has a category of",
)
}