From 27ff98f799ecde53015ff8ed0216aef55c6bfac2 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Fri, 11 Nov 2022 15:07:58 -0500 Subject: [PATCH 1/3] Move showUserSwitcher to UserInteractor This method will be needed by the status bar user chip, and has the equivalent behavior that was defined in `FooterActionsInteractor`. This CL moves the implementation + tests up from `FooterActionsInteractor` to a new dependency on `UserInteractor` Test: atest UserInteractorTest Bug: 254246505 Change-Id: Ia78513a0f86a5ade8727855f677919abac82b3b1 --- .../interactor/FooterActionsInteractor.kt | 26 +------ .../user/domain/interactor/UserInteractor.kt | 25 +++++++ .../domain/model/ShowDialogRequestModel.kt | 3 + .../user/ui/dialog/UserSwitchDialog.kt | 68 +++++++++++++++++++ .../dialog/UserSwitcherDialogCoordinator.kt | 22 ++++++ .../interactor/FooterActionsInteractorTest.kt | 56 --------------- .../domain/interactor/UserInteractorTest.kt | 58 +++++++++++++++- .../ui/viewmodel/UserSwitcherViewModelTest.kt | 2 + .../qs/footer/FooterActionsTestUtils.kt | 10 +-- 9 files changed, 183 insertions(+), 87 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 9ba3501c3434b..03bb7a0f45da1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -32,8 +32,6 @@ 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 @@ -42,10 +40,9 @@ 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 com.android.systemui.user.domain.interactor.UserInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -100,13 +97,12 @@ 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, + private val userInteractor: UserInteractor, securityRepository: SecurityRepository, foregroundServicesRepository: ForegroundServicesRepository, userSwitcherRepository: UserSwitcherRepository, @@ -182,22 +178,6 @@ constructor( } override fun showUserSwitcher(context: Context, expandable: Expandable) { - if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(context, expandable) - return - } - - val intent = - Intent(context, UserSwitcherActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - - activityStarter.startActivity( - intent, - true /* dismissShade */, - expandable.activityLaunchController(), - true /* showOverlockscreenwhenlocked */, - UserHandle.SYSTEM, - ) + userInteractor.showUserSwitcher(context, expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 516c6501c5ff7..bce91cb2d9ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -34,15 +34,19 @@ import android.util.Log import com.android.internal.util.UserIcons import com.android.systemui.R import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -81,6 +85,7 @@ constructor( private val repository: UserRepository, private val activityStarter: ActivityStarter, private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, @@ -468,6 +473,26 @@ constructor( } } + fun showUserSwitcher(context: Context, expandable: Expandable) { + if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog) + return + } + + val intent = + Intent(context, UserSwitcherActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + } + + activityStarter.startActivity( + intent, + true /* dismissShade */, + expandable.activityLaunchController(), + true /* showOverlockscreenwhenlocked */, + UserHandle.SYSTEM, + ) + } + private fun showDialog(request: ShowDialogRequestModel) { _dialogShowRequests.value = request } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 177356e6b5736..85c29647719b2 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -43,4 +43,7 @@ sealed class ShowDialogRequestModel( val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, override val dialogShower: UserSwitchDialogController.DialogShower?, ) : ShowDialogRequestModel(dialogShower) + + /** Show the user switcher dialog */ + object ShowUserSwitcherDialog : ShowDialogRequestModel() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt new file mode 100644 index 0000000000000..ed25898894359 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt @@ -0,0 +1,68 @@ +package com.android.systemui.user.ui.dialog + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.view.LayoutInflater +import com.android.internal.logging.UiEventLogger +import com.android.systemui.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** + * Extracted from the old UserSwitchDialogController. This is the dialog version of the full-screen + * user switcher. See config_enableFullscreenUserSwitcher + */ +class UserSwitchDialog( + context: Context, + adapter: UserDetailView.Adapter, + uiEventLogger: UiEventLogger, + falsingManager: FalsingManager, + activityStarter: ActivityStarter, + dialogLaunchAnimator: DialogLaunchAnimator, +) : SystemUIDialog(context) { + init { + setShowForAllUsers(true) + setCanceledOnTouchOutside(true) + setTitle(R.string.qs_user_switch_dialog_title) + setPositiveButton(R.string.quick_settings_done) { _, _ -> + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE) + } + setNeutralButton( + R.string.quick_settings_more_user_settings, + { _, _ -> + if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS) + val controller = + dialogLaunchAnimator.createActivityLaunchController( + getButton(BUTTON_NEUTRAL) + ) + + if (controller == null) { + dismiss() + } + + activityStarter.postStartActivityDismissingKeyguard( + USER_SETTINGS_INTENT, + 0, + controller + ) + } + }, + false /* dismissOnClick */ + ) + val gridFrame = + LayoutInflater.from(this.context).inflate(R.layout.qs_user_dialog_content, null) + setView(gridFrame) + + adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) + } + + companion object { + private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 58a4473186b39..41410542204c6 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -20,6 +20,7 @@ package com.android.systemui.user.ui.dialog import android.app.Dialog import android.content.Context import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.logging.UiEventLogger import com.android.settingslib.users.UserCreatingDialog import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj @@ -27,11 +28,14 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel import dagger.Lazy import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -47,6 +51,9 @@ constructor( private val broadcastSender: Lazy, private val dialogLaunchAnimator: Lazy, private val interactor: Lazy, + private val userDetailAdapterProvider: Provider, + private val eventLogger: Lazy, + private val activityStarter: Lazy, ) : CoreStartable { private var currentDialog: Dialog? = null @@ -108,6 +115,21 @@ constructor( INTERACTION_JANK_EXIT_GUEST_MODE_TAG, ), ) + is ShowDialogRequestModel.ShowUserSwitcherDialog -> + Pair( + UserSwitchDialog( + context = context.get(), + adapter = userDetailAdapterProvider.get(), + uiEventLogger = eventLogger.get(), + falsingManager = falsingManager.get(), + activityStarter = activityStarter.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_EXIT_GUEST_MODE_TAG, + ), + ) } currentDialog = dialog diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 2c2ddbb9b8c59..645b1cde632f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -16,10 +16,8 @@ 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 @@ -30,17 +28,13 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable -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 @@ -156,54 +150,4 @@ class FooterActionsInteractorTest : SysuiTestCase() { // 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() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - userSwitchDialogController = userSwitchDialogController, - ) - - val expandable = mock() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - verify(userSwitchDialogController).showDialog(context, expandable) - } - - @Test - fun showUserSwitcher_fullScreenEnabled() { - val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) } - val activityStarter = mock() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - activityStarter = activityStarter, - ) - - // The clicked expandable. - val expandable = mock() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - val intentCaptor = argumentCaptor() - 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, - ) - ) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 463f517d5063a..4b49420c99be8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.content.ComponentName import android.content.Intent import android.content.pm.UserInfo import android.graphics.Bitmap @@ -33,7 +34,10 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter @@ -41,6 +45,7 @@ import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -48,9 +53,11 @@ import com.android.systemui.user.domain.model.ShowDialogRequestModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel 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.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -90,6 +97,7 @@ class UserInteractorTest : SysuiTestCase() { private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var telephonyRepository: FakeTelephonyRepository + private lateinit var featureFlags: FakeFeatureFlags @Before fun setUp() { @@ -104,6 +112,7 @@ class UserInteractorTest : SysuiTestCase() { SUPERVISED_USER_CREATION_APP_PACKAGE, ) + featureFlags = FakeFeatureFlags() userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() telephonyRepository = FakeTelephonyRepository() @@ -147,7 +156,8 @@ class UserInteractorTest : SysuiTestCase() { uiEventLogger = uiEventLogger, resumeSessionReceiver = resumeSessionReceiver, resetOrExitSessionReceiver = resetOrExitSessionReceiver, - ) + ), + featureFlags = featureFlags, ) } @@ -715,6 +725,52 @@ class UserInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun `show user switcher - full screen disabled - shows dialog switcher`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + + var dialogRequest: ShowDialogRequestModel? = null + val expandable = mock() + underTest.showUserSwitcher(context, expandable) + + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + // Dialog is shown. + assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `show user switcher - full screen enabled - launches activity`() { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + + val expandable = mock() + underTest.showUserSwitcher(context, expandable) + + // Dialog is shown. + val intentCaptor = argumentCaptor() + 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, + ) + ) + } + private fun assertUsers( models: List?, count: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index db136800a3cc1..eac7fc21e5058 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter @@ -147,6 +148,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), + featureFlags = FakeFeatureFlags(), manager = manager, applicationScope = injectedScope, telephonyInteractor = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 325da4ead6667..63448e236867c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -28,8 +28,6 @@ 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 @@ -43,7 +41,6 @@ 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 @@ -54,6 +51,7 @@ 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.user.domain.interactor.UserInteractor import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings @@ -97,13 +95,12 @@ class FooterActionsTestUtils( /** 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(), + userInteractor: UserInteractor = mock(), securityRepository: SecurityRepository = securityRepository(), foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), @@ -112,13 +109,12 @@ class FooterActionsTestUtils( ): FooterActionsInteractor { return FooterActionsInteractorImpl( activityStarter, - featureFlags, metricsLogger, uiEventLogger, deviceProvisionedController, qsSecurityFooterUtils, fgsManagerController, - userSwitchDialogController, + userInteractor, securityRepository, foregroundServicesRepository, userSwitcherRepository, From 7292f2549bdfe53b0ae3cc3fe6c0b8cfb82b45e8 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Fri, 11 Nov 2022 15:17:14 -0500 Subject: [PATCH 2/3] Replace StatusBarUserSwitcherController with UserInteractor Originally, the user chip in the status bar was controlled by StatusBarUserSwitcherController, which was backed by UserInfoControllerImpl. This data source would sometimes show incorrect data for the current user name. Since UserRepository/UserInteractor exist now, this CL hooks up the existing StatusBarUserSwitcherContainer view to a new class, StatusBarUserChipViewModel, which is backed by the new repository class. This allows us to fix 2 bugs at once: 1. The chip now only shows when there are > 1 users AND the config is true 2. Hooking the chip up to the same data source that populates the user switcher screen means that the names will always be consistent Also removed the now-unused StatusBarUserSwitcherController, and its associated feature controller and tracker classes. Test: atest StatusBarUserChipViewModelTest Test: atest UserInteractorRefactoredTest Test: atest KeyguardStatusBarViewControllerTest Test: atest PhoneStatusBarViewControllerTest Bug: 254246505 Bug: 254680435 Change-Id: I3f5e212fed485b5697dedbe0510ab6c34d3b2ade --- .../dagger/KeyguardStatusBarViewModule.java | 9 - .../phone/KeyguardStatusBarView.java | 13 +- .../KeyguardStatusBarViewController.java | 24 +- .../statusbar/phone/PhoneStatusBarView.java | 8 + .../phone/PhoneStatusBarViewController.kt | 12 +- .../dagger/StatusBarFragmentModule.java | 9 - .../userswitcher/StatusBarUserInfoTracker.kt | 134 -------- .../StatusBarUserSwitcherController.kt | 112 ------- .../StatusBarUserSwitcherFeatureController.kt | 63 ---- .../user/data/repository/UserRepository.kt | 7 + .../user/domain/interactor/UserInteractor.kt | 3 + .../ui/binder/StatusBarUserChipViewBinder.kt | 65 ++++ .../viewmodel/StatusBarUserChipViewModel.kt | 59 ++++ .../KeyguardStatusBarViewControllerTest.java | 22 +- .../phone/PhoneStatusBarViewControllerTest.kt | 16 +- ...tusBarUserSwitcherControllerOldImplTest.kt | 102 ------ .../StatusBarUserChipViewModelTest.kt | 306 ++++++++++++++++++ .../data/repository/FakeUserRepository.kt | 2 + 18 files changed, 487 insertions(+), 479 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java index 8fc86004c400e..a7d4455b43c29 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java @@ -21,10 +21,7 @@ import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl; -import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -50,10 +47,4 @@ public abstract class KeyguardStatusBarViewModule { static StatusBarUserSwitcherContainer getUserSwitcherContainer(KeyguardStatusBarView view) { return view.findViewById(R.id.user_switcher_container); } - - /** */ - @Binds - @KeyguardStatusBarViewScope - abstract StatusBarUserSwitcherController bindStatusBarUserSwitcherController( - StatusBarUserSwitcherControllerImpl controller); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 7a49a495155b5..3c989a4ed3028 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -48,6 +48,9 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; +import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import java.io.PrintWriter; import java.util.ArrayList; @@ -70,7 +73,7 @@ public class KeyguardStatusBarView extends RelativeLayout { private ImageView mMultiUserAvatar; private BatteryMeterView mBatteryView; private StatusIconContainer mStatusIconContainer; - private ViewGroup mUserSwitcherContainer; + private StatusBarUserSwitcherContainer mUserSwitcherContainer; private boolean mKeyguardUserSwitcherEnabled; private boolean mKeyguardUserAvatarEnabled; @@ -121,8 +124,12 @@ public class KeyguardStatusBarView extends RelativeLayout { loadDimens(); } - public ViewGroup getUserSwitcherContainer() { - return mUserSwitcherContainer; + /** + * Should only be called from {@link KeyguardStatusBarViewController} + * @param viewModel view model for the status bar user chip + */ + void init(StatusBarUserChipViewModel viewModel) { + StatusBarUserChipViewBinder.bind(mUserSwitcherContainer, viewModel); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 14cebf4b9f4bc..d4dc1dc197a52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -59,13 +59,11 @@ import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt; import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.ViewController; import com.android.systemui.util.settings.SecureSettings; @@ -110,9 +108,7 @@ public class KeyguardStatusBarViewController extends ViewController mView.setKeyguardUserAvatarEnabled(!enabled)); + !mStatusBarUserChipViewModel.getChipEnabled()); mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r); mDisableStateTracker = new DisableStateTracker( @@ -344,11 +335,11 @@ public class KeyguardStatusBarViewController extends ViewController, @Named(UNFOLD_STATUS_BAR) private val progressProvider: Optional, - private val userSwitcherController: StatusBarUserSwitcherController, + private val userChipViewModel: StatusBarUserChipViewModel, private val viewUtil: ViewUtil, - private val configurationController: ConfigurationController + private val configurationController: ConfigurationController, ) { fun create( view: PhoneStatusBarView, @@ -168,7 +168,7 @@ class PhoneStatusBarViewController private constructor( view, progressProvider.getOrNull(), unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController(), - userSwitcherController, + userChipViewModel, viewUtil, touchEventHandler, configurationController diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java index 41f1f9589ce4c..efec27099dcd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java @@ -29,8 +29,6 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController; import com.android.systemui.statusbar.phone.StatusBarBoundsProvider; import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl; import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -39,7 +37,6 @@ import java.util.Set; import javax.inject.Named; -import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.Multibinds; @@ -125,12 +122,6 @@ public interface StatusBarFragmentModule { return view.findViewById(R.id.user_switcher_container); } - /** */ - @Binds - @StatusBarFragmentScope - StatusBarUserSwitcherController bindStatusBarUserSwitcherController( - StatusBarUserSwitcherControllerImpl controller); - /** */ @Provides @StatusBarFragmentScope diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt deleted file mode 100644 index f6b8cb0782cd5..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.userswitcher - -import android.graphics.drawable.Drawable -import android.os.UserManager -import com.android.systemui.Dumpable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.policy.CallbackController -import com.android.systemui.statusbar.policy.UserInfoController -import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener -import java.io.PrintWriter -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * Since every user switcher chip will user the exact same information and logic on whether or not - * to show, and what data to show, it makes sense to create a single tracker here - */ -@SysUISingleton -class StatusBarUserInfoTracker @Inject constructor( - private val userInfoController: UserInfoController, - private val userManager: UserManager, - private val dumpManager: DumpManager, - @Main private val mainExecutor: Executor, - @Background private val backgroundExecutor: Executor -) : CallbackController, Dumpable { - var currentUserName: String? = null - private set - var currentUserAvatar: Drawable? = null - private set - var userSwitcherEnabled = false - private set - private var listening = false - - private val listeners = mutableListOf() - - private val userInfoChangedListener = OnUserInfoChangedListener { name, picture, _ -> - currentUserAvatar = picture - currentUserName = name - notifyListenersUserInfoChanged() - } - - init { - dumpManager.registerDumpable(TAG, this) - } - - override fun addCallback(listener: CurrentUserChipInfoUpdatedListener) { - if (listeners.isEmpty()) { - startListening() - } - - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - - override fun removeCallback(listener: CurrentUserChipInfoUpdatedListener) { - listeners.remove(listener) - - if (listeners.isEmpty()) { - stopListening() - } - } - - private fun notifyListenersUserInfoChanged() { - listeners.forEach { - it.onCurrentUserChipInfoUpdated() - } - } - - private fun notifyListenersSettingChanged() { - listeners.forEach { - it.onStatusBarUserSwitcherSettingChanged(userSwitcherEnabled) - } - } - - private fun startListening() { - listening = true - userInfoController.addCallback(userInfoChangedListener) - } - - private fun stopListening() { - listening = false - userInfoController.removeCallback(userInfoChangedListener) - } - - /** - * Force a check to [UserManager.isUserSwitcherEnabled], and update listeners if the value has - * changed - */ - fun checkEnabled() { - backgroundExecutor.execute { - // Check on a background thread to avoid main thread Binder calls - val wasEnabled = userSwitcherEnabled - userSwitcherEnabled = userManager.isUserSwitcherEnabled - - if (wasEnabled != userSwitcherEnabled) { - mainExecutor.execute { - notifyListenersSettingChanged() - } - } - } - } - - override fun dump(pw: PrintWriter, args: Array) { - pw.println(" userSwitcherEnabled=$userSwitcherEnabled") - pw.println(" listening=$listening") - } -} - -interface CurrentUserChipInfoUpdatedListener { - fun onCurrentUserChipInfoUpdated() - fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) {} -} - -private const val TAG = "StatusBarUserInfoTracker" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt deleted file mode 100644 index e498ae4514006..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.userswitcher - -import android.content.Intent -import android.os.UserHandle -import android.view.View -import com.android.systemui.animation.Expandable -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager - -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.UserSwitcherActivity -import com.android.systemui.util.ViewController - -import javax.inject.Inject - -/** - * ViewController for [StatusBarUserSwitcherContainer] - */ -class StatusBarUserSwitcherControllerImpl @Inject constructor( - view: StatusBarUserSwitcherContainer, - private val tracker: StatusBarUserInfoTracker, - private val featureController: StatusBarUserSwitcherFeatureController, - private val userSwitcherDialogController: UserSwitchDialogController, - private val featureFlags: FeatureFlags, - private val activityStarter: ActivityStarter, - private val falsingManager: FalsingManager -) : ViewController(view), - StatusBarUserSwitcherController { - private val listener = object : CurrentUserChipInfoUpdatedListener { - override fun onCurrentUserChipInfoUpdated() { - updateChip() - } - - override fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) { - updateEnabled() - } - } - - private val featureFlagListener = object : OnUserSwitcherPreferenceChangeListener { - override fun onUserSwitcherPreferenceChange(enabled: Boolean) { - updateEnabled() - } - } - - public override fun onViewAttached() { - tracker.addCallback(listener) - featureController.addCallback(featureFlagListener) - mView.setOnClickListener { view: View -> - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - return@setOnClickListener - } - - if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - val intent = Intent(context, UserSwitcherActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - - activityStarter.startActivity(intent, true /* dismissShade */, - null /* ActivityLaunchAnimator.Controller */, - true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) - } else { - userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) - } - } - - updateEnabled() - } - - override fun onViewDetached() { - tracker.removeCallback(listener) - featureController.removeCallback(featureFlagListener) - mView.setOnClickListener(null) - } - - private fun updateChip() { - mView.text.text = tracker.currentUserName - mView.avatar.setImageDrawable(tracker.currentUserAvatar) - } - - private fun updateEnabled() { - if (featureController.isStatusBarUserSwitcherFeatureEnabled() && - tracker.userSwitcherEnabled) { - mView.visibility = View.VISIBLE - updateChip() - } else { - mView.visibility = View.GONE - } - } -} - -interface StatusBarUserSwitcherController { - fun init() -} - -private const val TAG = "SbUserSwitcherController" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt deleted file mode 100644 index 7bae9ff727603..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.userswitcher - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.statusbar.policy.CallbackController - -import javax.inject.Inject - -@SysUISingleton -class StatusBarUserSwitcherFeatureController @Inject constructor( - private val flags: FeatureFlags -) : CallbackController { - private val listeners = mutableListOf() - - init { - flags.addListener(Flags.STATUS_BAR_USER_SWITCHER) { - it.requestNoRestart() - notifyListeners() - } - } - - fun isStatusBarUserSwitcherFeatureEnabled(): Boolean { - return flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER) - } - - override fun addCallback(listener: OnUserSwitcherPreferenceChangeListener) { - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - - override fun removeCallback(listener: OnUserSwitcherPreferenceChangeListener) { - listeners.remove(listener) - } - - private fun notifyListeners() { - val enabled = flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER) - listeners.forEach { - it.onUserSwitcherPreferenceChange(enabled) - } - } -} - -interface OnUserSwitcherPreferenceChangeListener { - fun onUserSwitcherPreferenceChange(enabled: Boolean) -} diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index ed53de7dbee77..4c9b8e4639ca9 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -23,6 +23,7 @@ import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting +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 @@ -79,6 +80,9 @@ interface UserRepository { /** Whether we've scheduled the creation of a guest user. */ val isGuestUserCreationScheduled: AtomicBoolean + /** Whether to enable the status bar user chip (which launches the user switcher) */ + val isStatusBarUserChipEnabled: Boolean + /** The user of the secondary service. */ var secondaryUserId: Int @@ -127,6 +131,9 @@ constructor( override val isGuestUserCreationScheduled = AtomicBoolean() + override val isStatusBarUserChipEnabled: Boolean = + appContext.resources.getBoolean(R.bool.flag_user_switcher_chip) + override var secondaryUserId: Int = UserHandle.USER_NULL override var isRefreshUsersPaused: Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index bce91cb2d9ae0..83f0711caa38c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -261,6 +261,9 @@ constructor( /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + /** Whether to enable the user chip in the status bar */ + val isStatusBarUserChipEnabled: Boolean = repository.isStatusBarUserChipEnabled + private val _dialogShowRequests = MutableStateFlow(null) val dialogShowRequests: Flow = _dialogShowRequests.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt new file mode 100644 index 0000000000000..8e40f68e27e92 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.ui.binder + +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.animation.Expandable +import com.android.systemui.common.ui.binder.TextViewBinder +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@OptIn(InternalCoroutinesApi::class) +object StatusBarUserChipViewBinder { + /** Binds the status bar user chip view model to the given view */ + @JvmStatic + fun bind( + view: StatusBarUserSwitcherContainer, + viewModel: StatusBarUserChipViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.isChipVisible.collect { isVisible -> view.isVisible = isVisible } + } + + launch { + viewModel.userName.collect { name -> TextViewBinder.bind(view.text, name) } + } + + launch { + viewModel.userAvatar.collect { avatar -> view.avatar.setImageDrawable(avatar) } + } + + bindButton(view, viewModel) + } + } + } + + private fun bindButton( + view: StatusBarUserSwitcherContainer, + viewModel: StatusBarUserChipViewModel, + ) { + view.setOnClickListener { viewModel.onClick(Expandable.fromView(view)) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt new file mode 100644 index 0000000000000..3300e8e5b2a5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt @@ -0,0 +1,59 @@ +/* + * 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.user.ui.viewmodel + +import android.content.Context +import android.graphics.drawable.Drawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Text +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.user.domain.interactor.UserInteractor +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +class StatusBarUserChipViewModel +@Inject +constructor( + @Application private val context: Context, + interactor: UserInteractor, +) { + /** Whether the status bar chip ui should be available */ + val chipEnabled: Boolean = interactor.isStatusBarUserChipEnabled + + /** Whether or not the chip should be showing, based on the number of users */ + val isChipVisible: Flow = + if (!chipEnabled) { + flowOf(false) + } else { + interactor.users.mapLatest { users -> users.size > 1 } + } + + /** The display name of the current user */ + val userName: Flow = interactor.selectedUser.mapLatest { userModel -> userModel.name } + + /** Avatar for the current user */ + val userAvatar: Flow = + interactor.selectedUser.mapLatest { userModel -> userModel.image } + + /** Action to execute on click. Should launch the user switcher */ + val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 6ec5cf82a81e6..eb0b9b3a3fb15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -56,14 +56,12 @@ import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -112,16 +110,12 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private StatusBarContentInsetsProvider mStatusBarContentInsetsProvider; @Mock private UserManager mUserManager; + @Mock + private StatusBarUserChipViewModel mStatusBarUserChipViewModel; @Captor private ArgumentCaptor mConfigurationListenerCaptor; @Captor private ArgumentCaptor mKeyguardCallbackCaptor; - @Mock - private StatusBarUserSwitcherFeatureController mStatusBarUserSwitcherFeatureController; - @Mock - private StatusBarUserSwitcherController mStatusBarUserSwitcherController; - @Mock - private StatusBarUserInfoTracker mStatusBarUserInfoTracker; @Mock private SecureSettings mSecureSettings; @Mock private CommandQueue mCommandQueue; @Mock private KeyguardLogger mLogger; @@ -169,9 +163,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mStatusBarStateController, mStatusBarContentInsetsProvider, mUserManager, - mStatusBarUserSwitcherFeatureController, - mStatusBarUserSwitcherController, - mStatusBarUserInfoTracker, + mStatusBarUserChipViewModel, mSecureSettings, mCommandQueue, mFakeExecutor, @@ -479,8 +471,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Test public void testNewUserSwitcherDisablesAvatar_newUiOn() { // GIVEN the status bar user switcher chip is enabled - when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled()) - .thenReturn(true); + when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(true); // WHEN the controller is created mController = createController(); @@ -492,8 +483,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Test public void testNewUserSwitcherDisablesAvatar_newUiOff() { // GIVEN the status bar user switcher chip is disabled - when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled()) - .thenReturn(false); + when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(false); // WHEN the controller is created mController = createController(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index a61fba5c40000..320a083158433 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -27,11 +27,11 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.shade.NotificationPanelViewController -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel import com.android.systemui.util.mockito.any import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -64,7 +64,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock - private lateinit var userSwitcherController: StatusBarUserSwitcherController + private lateinit var userChipViewModel: StatusBarUserChipViewModel @Mock private lateinit var viewUtil: ViewUtil @@ -79,14 +79,13 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { `when`(notificationPanelViewController.view).thenReturn(panelView) `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController()) .thenReturn(moveFromCenterAnimation) - // create the view on main thread as it requires main looper + // create the view and controller on main thread as it requires main looper InstrumentationRegistry.getInstrumentation().runOnMainSync { val parent = FrameLayout(mContext) // add parent to keep layout params view = LayoutInflater.from(mContext) .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView + controller = createAndInitController(view) } - - controller = createAndInitController(view) } @Test @@ -106,7 +105,10 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { val view = createViewMock() val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) unfoldConfig.isEnabled = true - controller = createAndInitController(view) + // create the controller on main thread as it requires main looper + InstrumentationRegistry.getInstrumentation().runOnMainSync { + controller = createAndInitController(view) + } verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) argumentCaptor.value.onPreDraw() @@ -126,7 +128,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { return PhoneStatusBarViewController.Factory( Optional.of(sysuiUnfoldComponent), Optional.of(progressProvider), - userSwitcherController, + userChipViewModel, viewUtil, configurationController ).create(view, touchEventHandler).also { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt deleted file mode 100644 index eba3b04f3472c..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.userswitcher - -import android.content.Intent -import android.os.UserHandle -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -@SmallTest -class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { - @Mock - private lateinit var tracker: StatusBarUserInfoTracker - - @Mock - private lateinit var featureController: StatusBarUserSwitcherFeatureController - - @Mock - private lateinit var userSwitcherDialogController: UserSwitchDialogController - - @Mock - private lateinit var featureFlags: FeatureFlags - - @Mock - private lateinit var activityStarter: ActivityStarter - - @Mock - private lateinit var falsingManager: FalsingManager - - private lateinit var statusBarUserSwitcherContainer: StatusBarUserSwitcherContainer - private lateinit var controller: StatusBarUserSwitcherControllerImpl - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - statusBarUserSwitcherContainer = StatusBarUserSwitcherContainer(mContext, null) - statusBarUserSwitcherContainer - controller = StatusBarUserSwitcherControllerImpl( - statusBarUserSwitcherContainer, - tracker, - featureController, - userSwitcherDialogController, - featureFlags, - activityStarter, - falsingManager - ) - controller.init() - controller.onViewAttached() - } - - @Test - fun testFalsingManager() { - statusBarUserSwitcherContainer.callOnClick() - verify(falsingManager).isFalseTap(FalsingManager.LOW_PENALTY) - } - - @Test - fun testStartActivity() { - `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) - statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(), any()) - `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) - statusBarUserSwitcherContainer.callOnClick() - verify(activityStarter).startActivity(any(Intent::class.java), - eq(true) /* dismissShade */, - eq(null) /* animationController */, - eq(true) /* showOverLockscreenWhenLocked */, - eq(UserHandle.SYSTEM)) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt new file mode 100644 index 0000000000000..db348b8029a02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -0,0 +1,306 @@ +/* + * 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.user.ui.viewmodel + +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class StatusBarUserChipViewModelTest : SysuiTestCase() { + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + + private lateinit var underTest: StatusBarUserChipViewModel + + private val userRepository = FakeUserRepository() + private val keyguardRepository = FakeKeyguardRepository() + private val featureFlags = FakeFeatureFlags() + private lateinit var guestUserInteractor: GuestUserInteractor + private lateinit var refreshUsersScheduler: RefreshUsersScheduler + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + doAnswer { invocation -> + val userId = invocation.arguments[0] as Int + when (userId) { + USER_ID_0 -> return@doAnswer USER_IMAGE_0 + USER_ID_1 -> return@doAnswer USER_IMAGE_1 + USER_ID_2 -> return@doAnswer USER_IMAGE_2 + else -> return@doAnswer mock() + } + } + .`when`(manager) + .getUserIcon(anyInt()) + + userRepository.isStatusBarUserChipEnabled = true + + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + repository = userRepository, + ) + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, + ) + + underTest = viewModel() + } + + @Test + fun `config is false - chip is disabled`() { + // the enabled bit is set at SystemUI startup, so recreate the view model here + userRepository.isStatusBarUserChipEnabled = false + underTest = viewModel() + + assertThat(underTest.chipEnabled).isFalse() + } + + @Test + fun `config is true - chip is enabled`() { + // the enabled bit is set at SystemUI startup, so recreate the view model here + userRepository.isStatusBarUserChipEnabled = true + underTest = viewModel() + + assertThat(underTest.chipEnabled).isTrue() + } + + @Test + fun `should show chip criteria - single user`() = + testScope.runTest { + userRepository.setUserInfos(listOf(USER_0)) + userRepository.setSelectedUserInfo(USER_0) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val values = mutableListOf() + + val job = launch { underTest.isChipVisible.toList(values) } + advanceUntilIdle() + + assertThat(values).containsExactly(false) + + job.cancel() + } + + @Test + fun `should show chip criteria - multiple users`() = + testScope.runTest { + setMultipleUsers() + + var latest: Boolean? = null + val job = underTest.isChipVisible.onEach { latest = it }.launchIn(this) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun `user chip name - shows selected user info`() = + testScope.runTest { + setMultipleUsers() + + var latest: Text? = null + val job = underTest.userName.onEach { latest = it }.launchIn(this) + + userRepository.setSelectedUserInfo(USER_0) + assertThat(latest).isEqualTo(USER_NAME_0) + + userRepository.setSelectedUserInfo(USER_1) + assertThat(latest).isEqualTo(USER_NAME_1) + + userRepository.setSelectedUserInfo(USER_2) + assertThat(latest).isEqualTo(USER_NAME_2) + + job.cancel() + } + + @Test + fun `user chip avatar - shows selected user info`() = + testScope.runTest { + setMultipleUsers() + + // A little hacky. System server passes us bitmaps and we wrap them in the interactor. + // Unwrap them to make sure we're always tracking the current user's bitmap + var latest: Bitmap? = null + val job = + underTest.userAvatar + .onEach { + if (it !is BitmapDrawable) { + latest = null + } + + latest = (it as BitmapDrawable).bitmap + } + .launchIn(this) + + userRepository.setSelectedUserInfo(USER_0) + assertThat(latest).isEqualTo(USER_IMAGE_0) + + userRepository.setSelectedUserInfo(USER_1) + assertThat(latest).isEqualTo(USER_IMAGE_1) + + userRepository.setSelectedUserInfo(USER_2) + assertThat(latest).isEqualTo(USER_IMAGE_2) + + job.cancel() + } + + private fun viewModel(): StatusBarUserChipViewModel { + return StatusBarUserChipViewModel( + context = context, + interactor = + UserInteractor( + applicationContext = context, + repository = userRepository, + activityStarter = activityStarter, + keyguardInteractor = + KeyguardInteractor( + repository = keyguardRepository, + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = testScope.backgroundScope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = testDispatcher, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = guestUserInteractor, + ) + ) + } + + private suspend fun setMultipleUsers() { + userRepository.setUserInfos(listOf(USER_0, USER_1, USER_2)) + userRepository.setSelectedUserInfo(USER_0) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + } + + companion object { + private const val USER_ID_0 = 0 + private val USER_IMAGE_0 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_0 = Text.Loaded("zero") + + private const val USER_ID_1 = 1 + private val USER_IMAGE_1 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_1 = Text.Loaded("one") + + private const val USER_ID_2 = 2 + private val USER_IMAGE_2 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_2 = Text.Loaded("two") + + private val USER_0 = + UserInfo( + USER_ID_0, + USER_NAME_0.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + + private val USER_1 = + UserInfo( + USER_ID_1, + USER_NAME_1.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + + private val USER_2 = + UserInfo( + USER_ID_2, + USER_NAME_2.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index b7c8cbf40bea4..ea5a302ce05ae 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -49,6 +49,8 @@ class FakeUserRepository : UserRepository { override val isGuestUserCreationScheduled = AtomicBoolean() + override var isStatusBarUserChipEnabled: Boolean = false + override var secondaryUserId: Int = UserHandle.USER_NULL override var isRefreshUsersPaused: Boolean = false From ba8376ca2ceb45e9b9ab8eba35cfeea5ce610704 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Fri, 11 Nov 2022 15:23:24 -0500 Subject: [PATCH 3/3] Update status bar user chip view spec Modified the margins, padding, and size of the user chip to match the spec. Also use the outer status icon container for view margin adjustments in KeyguardStatusBarView, since the margin adjustments only used to work because the status icon container was always the first item in the linear layout. Test: visual on keyguard and unlocked status bar Bug: 231688534 Change-Id: I8d766f4a2c630a3cbab7f0e5e7362652eb3e0478 --- .../res/layout/keyguard_status_bar.xml | 25 ++---------- packages/SystemUI/res/layout/status_bar.xml | 25 ++---------- .../layout/status_bar_user_chip_container.xml | 40 +++++++++++++++++++ packages/SystemUI/res/values/dimens.xml | 5 +++ packages/SystemUI/res/values/styles.xml | 6 +++ .../phone/KeyguardStatusBarView.java | 10 +---- 6 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 packages/SystemUI/res/layout/status_bar_user_chip_container.xml diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml index d27fa192e7411..8b85940328167 100644 --- a/packages/SystemUI/res/layout/keyguard_status_bar.xml +++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml @@ -34,30 +34,13 @@ android:paddingTop="@dimen/status_bar_padding_top" android:layout_alignParentEnd="true" android:gravity="center_vertical|end" > - - - - - + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> - - - - - + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> diff --git a/packages/SystemUI/res/layout/status_bar_user_chip_container.xml b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml new file mode 100644 index 0000000000000..b374074958cb3 --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7cda9d70ea49a..6fedb4f600d02 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1404,6 +1404,11 @@ 4dp 28dp + + 16dp + 12dp + 12sp + 662dp diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index dea06b7a00e66..b11b6d633f14a 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -23,6 +23,12 @@ @color/status_bar_clock_color + + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 3c989a4ed3028..13566ef8c6304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -311,10 +311,7 @@ public class KeyguardStatusBarView extends RelativeLayout { lp = (LayoutParams) mStatusIconArea.getLayoutParams(); lp.removeRule(RelativeLayout.RIGHT_OF); lp.width = LayoutParams.WRAP_CONTENT; - - LinearLayout.LayoutParams llp = - (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams(); - llp.setMarginStart(getResources().getDimensionPixelSize( + lp.setMarginStart(getResources().getDimensionPixelSize( R.dimen.system_icons_super_container_margin_start)); return true; } @@ -346,10 +343,7 @@ public class KeyguardStatusBarView extends RelativeLayout { lp = (LayoutParams) mStatusIconArea.getLayoutParams(); lp.addRule(RelativeLayout.RIGHT_OF, R.id.cutout_space_view); lp.width = LayoutParams.MATCH_PARENT; - - LinearLayout.LayoutParams llp = - (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams(); - llp.setMarginStart(0); + lp.setMarginStart(0); return true; }