diff options
| author | 2022-11-11 15:17:14 -0500 | |
|---|---|---|
| committer | 2022-11-16 16:06:36 -0500 | |
| commit | 7292f2549bdfe53b0ae3cc3fe6c0b8cfb82b45e8 (patch) | |
| tree | 53954b7e1409644c964053cf966e23c08ddf5730 | |
| parent | 27ff98f799ecde53015ff8ed0216aef55c6bfac2 (diff) | |
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
18 files changed, 487 insertions, 479 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java index 8fc86004c400..a7d4455b43c2 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 7a49a495155b..3c989a4ed302 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 14cebf4b9f4b..d4dc1dc197a5 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<KeyguardStat private final SysuiStatusBarStateController mStatusBarStateController; private final StatusBarContentInsetsProvider mInsetsProvider; private final UserManager mUserManager; - private final StatusBarUserSwitcherFeatureController mFeatureController; - private final StatusBarUserSwitcherController mUserSwitcherController; - private final StatusBarUserInfoTracker mStatusBarUserInfoTracker; + private final StatusBarUserChipViewModel mStatusBarUserChipViewModel; private final SecureSettings mSecureSettings; private final CommandQueue mCommandQueue; private final Executor mMainExecutor; @@ -276,9 +272,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat SysuiStatusBarStateController statusBarStateController, StatusBarContentInsetsProvider statusBarContentInsetsProvider, UserManager userManager, - StatusBarUserSwitcherFeatureController featureController, - StatusBarUserSwitcherController userSwitcherController, - StatusBarUserInfoTracker statusBarUserInfoTracker, + StatusBarUserChipViewModel userChipViewModel, SecureSettings secureSettings, CommandQueue commandQueue, @Main Executor mainExecutor, @@ -301,9 +295,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mStatusBarStateController = statusBarStateController; mInsetsProvider = statusBarContentInsetsProvider; mUserManager = userManager; - mFeatureController = featureController; - mUserSwitcherController = userSwitcherController; - mStatusBarUserInfoTracker = statusBarUserInfoTracker; + mStatusBarUserChipViewModel = userChipViewModel; mSecureSettings = secureSettings; mCommandQueue = commandQueue; mMainExecutor = mainExecutor; @@ -328,8 +320,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat R.dimen.header_notifications_collide_distance); mView.setKeyguardUserAvatarEnabled( - !mFeatureController.isStatusBarUserSwitcherFeatureEnabled()); - mFeatureController.addCallback(enabled -> mView.setKeyguardUserAvatarEnabled(!enabled)); + !mStatusBarUserChipViewModel.getChipEnabled()); mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r); mDisableStateTracker = new DisableStateTracker( @@ -344,11 +335,11 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat super.onInit(); mCarrierTextController.init(); mBatteryMeterViewController.init(); - mUserSwitcherController.init(); } @Override protected void onViewAttached() { + mView.init(mStatusBarUserChipViewModel); mConfigurationController.addCallback(mConfigurationListener); mAnimationScheduler.addCallback(mAnimationCallback); mUserInfoController.addCallback(mOnUserInfoChangedListener); @@ -394,9 +385,6 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Sets whether user switcher is enabled. */ public void setKeyguardUserSwitcherEnabled(boolean enabled) { mView.setKeyguardUserSwitcherEnabled(enabled); - // We don't have a listener for when the user switcher setting changes, so this is - // where we re-check the state - mStatusBarUserInfoTracker.checkEnabled(); } /** Sets whether this controller should listen to battery updates. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 7aeb08dd5ddb..28bc64de2cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -38,6 +38,9 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; 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 com.android.systemui.util.leak.RotationUtils; import java.util.Objects; @@ -73,6 +76,11 @@ public class PhoneStatusBarView extends FrameLayout { mTouchEventHandler = handler; } + void init(StatusBarUserChipViewModel viewModel) { + StatusBarUserSwitcherContainer container = findViewById(R.id.user_switcher_container); + StatusBarUserChipViewBinder.bind(container, viewModel); + } + @Override public void onFinishInflate() { super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index f9c4c8f3b4fe..a6c2b2c2771c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -23,11 +23,11 @@ import android.view.ViewGroup import android.view.ViewTreeObserver import com.android.systemui.R import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator -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.UNFOLD_STATUS_BAR import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel import com.android.systemui.util.ViewController import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.view.ViewUtil @@ -40,7 +40,7 @@ class PhoneStatusBarViewController private constructor( view: PhoneStatusBarView, @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, - private val userSwitcherController: StatusBarUserSwitcherController, + private val userChipViewModel: StatusBarUserChipViewModel, private val viewUtil: ViewUtil, touchEventHandler: PhoneStatusBarView.TouchEventHandler, private val configurationController: ConfigurationController @@ -91,10 +91,10 @@ class PhoneStatusBarViewController private constructor( init { mView.setTouchEventHandler(touchEventHandler) + mView.init(userChipViewModel) } override fun onInit() { - userSwitcherController.init() } fun setImportantForAccessibility(mode: Int) { @@ -156,9 +156,9 @@ class PhoneStatusBarViewController private constructor( private val unfoldComponent: Optional<SysUIUnfoldComponent>, @Named(UNFOLD_STATUS_BAR) private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>, - 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 41f1f9589ce4..efec27099dcd 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; @@ -126,12 +123,6 @@ public interface StatusBarFragmentModule { } /** */ - @Binds - @StatusBarFragmentScope - StatusBarUserSwitcherController bindStatusBarUserSwitcherController( - StatusBarUserSwitcherControllerImpl controller); - - /** */ @Provides @StatusBarFragmentScope static PhoneStatusBarViewController providePhoneStatusBarViewController( 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 f6b8cb0782cd..000000000000 --- 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<CurrentUserChipInfoUpdatedListener>, 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<CurrentUserChipInfoUpdatedListener>() - - 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<out String>) { - 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 e498ae451400..000000000000 --- 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<StatusBarUserSwitcherContainer>(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 7bae9ff72760..000000000000 --- 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<OnUserSwitcherPreferenceChangeListener> { - private val listeners = mutableListOf<OnUserSwitcherPreferenceChangeListener>() - - 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 ed53de7dbee7..4c9b8e4639ca 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 bce91cb2d9ae..83f0711caa38 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<ShowDialogRequestModel?>(null) val dialogShowRequests: Flow<ShowDialogRequestModel?> = _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 000000000000..8e40f68e27e9 --- /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 000000000000..3300e8e5b2a5 --- /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<Boolean> = + if (!chipEnabled) { + flowOf(false) + } else { + interactor.users.mapLatest { users -> users.size > 1 } + } + + /** The display name of the current user */ + val userName: Flow<Text> = interactor.selectedUser.mapLatest { userModel -> userModel.name } + + /** Avatar for the current user */ + val userAvatar: Flow<Drawable> = + 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 6ec5cf82a81e..eb0b9b3a3fb1 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<ConfigurationListener> mConfigurationListenerCaptor; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> 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 a61fba5c4000..320a08315843 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 eba3b04f3472..000000000000 --- 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 000000000000..db348b8029a0 --- /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<Bitmap>() + } + } + .`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<Boolean>() + + 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 b7c8cbf40bea..ea5a302ce05a 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 |