diff options
31 files changed, 730 insertions, 617 deletions
diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml index d27fa192e741..8b8594032816 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" > - <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + + <include android:id="@+id/user_switcher_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingBottom="4dp" - android:paddingStart="8dp" - android:paddingEnd="8dp" - android:background="@drawable/status_bar_user_chip_bg" - android:visibility="visible" > - <ImageView android:id="@+id/current_user_avatar" - android:layout_width="@dimen/multi_user_avatar_keyguard_size" - android:layout_height="@dimen/multi_user_avatar_keyguard_size" - android:scaleType="centerInside" - android:paddingEnd="4dp" /> - - <TextView android:id="@+id/current_user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" - /> - </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> <FrameLayout android:id="@+id/system_icons_container" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index e281511140c7..b63ee5095554 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -136,31 +136,12 @@ android:gravity="center_vertical|end" android:clipChildren="false"> - <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + <include android:id="@+id/user_switcher_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingBottom="4dp" - android:paddingStart="8dp" - android:paddingEnd="8dp" - android:layout_marginEnd="16dp" - android:background="@drawable/status_bar_user_chip_bg" - android:visibility="visible" > - <ImageView android:id="@+id/current_user_avatar" - android:layout_width="@dimen/multi_user_avatar_keyguard_size" - android:layout_height="@dimen/multi_user_avatar_keyguard_size" - android:scaleType="centerInside" - android:paddingEnd="4dp" /> - - <TextView android:id="@+id/current_user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" - /> - </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> <include layout="@layout/system_icons" /> </com.android.keyguard.AlphaOptimizedLinearLayout> 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 000000000000..b374074958cb --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/user_switcher_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal" + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + android:background="@drawable/status_bar_user_chip_bg" + android:visibility="visible" > + <ImageView android:id="@+id/current_user_avatar" + android:layout_width="@dimen/status_bar_user_chip_avatar_size" + android:layout_height="@dimen/status_bar_user_chip_avatar_size" + android:layout_margin="4dp" + android:scaleType="centerInside" /> + + <TextView android:id="@+id/current_user_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="8dp" + android:textAppearance="@style/TextAppearance.StatusBar.UserChip" + /> +</com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7cda9d70ea49..6fedb4f600d0 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1404,6 +1404,11 @@ <dimen name="ongoing_call_chip_icon_text_padding">4dp</dimen> <dimen name="ongoing_call_chip_corner_radius">28dp</dimen> + <!-- Status bar user chip --> + <dimen name="status_bar_user_chip_avatar_size">16dp</dimen> + <dimen name="status_bar_user_chip_end_margin">12dp</dimen> + <dimen name="status_bar_user_chip_text_size">12sp</dimen> + <!-- Internet panel related dimensions --> <dimen name="internet_dialog_list_max_height">662dp</dimen> <!-- The height of the WiFi network in Internet panel. --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index dea06b7a00e6..b11b6d633f14 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -23,6 +23,12 @@ <item name="android:textColor">@color/status_bar_clock_color</item> </style> + <style name="TextAppearance.StatusBar.UserChip" parent="@*android:style/TextAppearance.StatusBar.Icon"> + <item name="android:textSize">@dimen/status_bar_user_chip_text_size</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> + <item name="android:textColor">@color/status_bar_clock_color</item> + </style> + <style name="TextAppearance.StatusBar.Expanded" parent="@*android:style/TextAppearance.StatusBar"> <item name="android:textColor">?android:attr/textColorTertiary</item> </style> 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/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 9ba3501c3434..03bb7a0f45da 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/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 7a49a495155b..13566ef8c630 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 @@ -304,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; } @@ -339,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; } 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 516c6501c5ff..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 @@ -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, @@ -256,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() @@ -468,6 +476,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 177356e6b573..85c29647719b 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/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/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt new file mode 100644 index 000000000000..ed2589889435 --- /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 58a4473186b3..41410542204c 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<BroadcastSender>, private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, private val interactor: Lazy<UserInteractor>, + private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>, + private val eventLogger: Lazy<UiEventLogger>, + private val activityStarter: Lazy<ActivityStarter>, ) : 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/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/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 2c2ddbb9b8c5..645b1cde632f 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<UserSwitchDialogController>() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - userSwitchDialogController = userSwitchDialogController, - ) - - val expandable = mock<Expandable>() - 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<ActivityStarter>() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - activityStarter = activityStarter, - ) - - // The clicked expandable. - val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - val intentCaptor = argumentCaptor<Intent>() - verify(activityStarter) - .startActivity( - intentCaptor.capture(), - /* dismissShade= */ eq(true), - /* ActivityLaunchAnimator.Controller= */ nullable(), - /* showOverLockscreenWhenLocked= */ eq(true), - eq(UserHandle.SYSTEM), - ) - assertThat(intentCaptor.value.component) - .isEqualTo( - ComponentName( - context, - UserSwitcherActivity::class.java, - ) - ) - } } 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/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 463f517d5063..4b49420c99be 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<Expandable>() + 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<Expandable>() + underTest.showUserSwitcher(context, expandable) + + // Dialog is shown. + val intentCaptor = argumentCaptor<Intent>() + verify(activityStarter) + .startActivity( + intentCaptor.capture(), + /* dismissShade= */ eq(true), + /* ActivityLaunchAnimator.Controller= */ nullable(), + /* showOverLockscreenWhenLocked= */ eq(true), + eq(UserHandle.SYSTEM), + ) + assertThat(intentCaptor.value.component) + .isEqualTo( + ComponentName( + context, + UserSwitcherActivity::class.java, + ) + ) + } + private fun assertUsers( models: List<UserModel>?, count: Int, 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/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index db136800a3cc..eac7fc21e505 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 325da4ead666..63448e236867 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, 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 |