Implementation of the FooterActions following the MAD (1/3) am: 6fb22d4b16
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19678215 Change-Id: I71cb9668445f277fadc69294e60bf2ff63a46cca Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
f8aaf7ab5f
@@ -230,6 +230,7 @@ android_library {
|
||||
libs: [
|
||||
"android.test.runner",
|
||||
"android.test.base",
|
||||
"android.test.mock",
|
||||
],
|
||||
kotlincflags: ["-Xjvm-default=enable"],
|
||||
aaptflags: [
|
||||
|
||||
@@ -311,8 +311,7 @@ class ActivityLaunchAnimator(
|
||||
@JvmStatic
|
||||
fun fromView(view: View, cujType: Int? = null): Controller? {
|
||||
if (view.parent !is ViewGroup) {
|
||||
// TODO(b/192194319): Throw instead of just logging.
|
||||
Log.wtf(
|
||||
Log.e(
|
||||
TAG,
|
||||
"Skipping animation as view $view is not attached to a ViewGroup",
|
||||
Exception()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ android_library {
|
||||
"testables",
|
||||
"truth-prebuilt",
|
||||
"androidx.test.uiautomator",
|
||||
"kotlinx_coroutines_test",
|
||||
],
|
||||
|
||||
libs: [
|
||||
"android.test.mock",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)?,
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -10,12 +10,10 @@ import android.graphics.Rect
|
||||
import android.os.Looper
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import android.util.Log
|
||||
import android.view.IRemoteAnimationFinishedCallback
|
||||
import android.view.RemoteAnimationAdapter
|
||||
import android.view.RemoteAnimationTarget
|
||||
import android.view.SurfaceControl
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.test.filters.SmallTest
|
||||
@@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
|
||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||
@Mock lateinit var failHandler: Log.TerribleFailureHandler
|
||||
|
||||
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
|
||||
@get:Rule val rule = MockitoJUnit.rule()
|
||||
@@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
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 {
|
||||
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
||||
val taskInfo = ActivityManager.RunningTaskInfo()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user