Change Fullscreen UserSwitcher into a dialog
This is done to retain the last-active-app-resumption when switching users. Also mitigates some issues with the previous activity behavior in "lockTaskMode". Bug: b/270495254 Fixes: b/274122173 Test: UserInteractorTest.kt Test: manual - switch user from chip bar to other non-supervised user Test: manual - switch user to locked-down supervised user. verify that user switcher still appears on locked-down child user and that the user can switch back to the main user. Test: manual - switch user to guest user, and back Test: manual - open fullscreen user switcher, turn off screen. verify that user switcher is dismissed. Test: manual - verify user switcher fullscreen is the expected UI in portrait and landscape. Change-Id: Icc65e464c4f08817fc3668b2e898dadfc7cea4aa
This commit is contained in:
@@ -893,18 +893,6 @@
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".user.UserSwitcherActivity"
|
||||
android:label="@string/accessibility_multi_user_switch_switcher"
|
||||
android:theme="@style/Theme.UserSwitcherActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:showWhenLocked="true"
|
||||
android:showForAllUsers="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:lockTaskMode="always"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".controls.management.ControlsRequestReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
||||
@@ -6,7 +6,7 @@ Multiple users and the ability to switch between them is controlled by Settings
|
||||
|
||||
### Quick Settings
|
||||
|
||||
In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherActivity][5]).
|
||||
In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherFullscreenDialog][5]).
|
||||
|
||||
### Bouncer
|
||||
|
||||
@@ -29,7 +29,7 @@ All visual implementations should derive their logic and use the adapter specifi
|
||||
|
||||
## Visual Components
|
||||
|
||||
### [UserSwitcherActivity][5]
|
||||
### [UserSwitcherFullscreenDialog][5]
|
||||
|
||||
A fullscreen user switching activity, supporting add guest/user actions if configured.
|
||||
|
||||
@@ -41,5 +41,5 @@ Renders user switching as a dialog over the current surface, and supports add gu
|
||||
[2]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserController.java
|
||||
[3]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
|
||||
[4]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
|
||||
[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
|
||||
[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt
|
||||
[6]: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
|
||||
|
||||
@@ -836,12 +836,10 @@
|
||||
<item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.UserSwitcherActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar">
|
||||
<style name="Theme.UserSwitcherFullscreenDialog" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
|
||||
<item name="android:statusBarColor">@color/user_switcher_fullscreen_bg</item>
|
||||
<item name="android:windowBackground">@color/user_switcher_fullscreen_bg</item>
|
||||
<item name="android:navigationBarColor">@color/user_switcher_fullscreen_bg</item>
|
||||
<!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen -->
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.CreateUser" parent="@android:style/Theme.DeviceDefault.NoActionBar">
|
||||
|
||||
@@ -89,7 +89,7 @@ interface FooterActionsInteractor {
|
||||
fun showSettings(expandable: Expandable)
|
||||
|
||||
/** Show the user switcher. */
|
||||
fun showUserSwitcher(context: Context, expandable: Expandable)
|
||||
fun showUserSwitcher(expandable: Expandable)
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
@@ -177,7 +177,7 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun showUserSwitcher(context: Context, expandable: Expandable) {
|
||||
userInteractor.showUserSwitcher(context, expandable)
|
||||
override fun showUserSwitcher(expandable: Expandable) {
|
||||
userInteractor.showUserSwitcher(expandable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ class FooterActionsViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showUserSwitcher(context, expandable)
|
||||
footerActionsInteractor.showUserSwitcher(expandable)
|
||||
}
|
||||
|
||||
private fun onSettingsButtonClicked(expandable: Expandable) {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.android.systemui.user;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.android.settingslib.users.EditUserInfoController;
|
||||
@@ -24,11 +23,8 @@ import com.android.systemui.user.data.repository.UserRepositoryModule;
|
||||
import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule;
|
||||
import com.android.systemui.user.ui.dialog.UserDialogModule;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.ClassKey;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
/**
|
||||
* Dagger module for User related classes.
|
||||
@@ -49,12 +45,6 @@ public abstract class UserModule {
|
||||
return new EditUserInfoController(FILE_PROVIDER_AUTHORITY);
|
||||
}
|
||||
|
||||
/** Provides UserSwitcherActivity */
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(UserSwitcherActivity.class)
|
||||
public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity);
|
||||
|
||||
/**
|
||||
* Provides the {@link UserHandle} for the user associated with this System UI process.
|
||||
*
|
||||
|
||||
@@ -1,57 +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.user
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowInsets.Type
|
||||
import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
import dagger.Lazy
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Support a fullscreen user switcher */
|
||||
open class UserSwitcherActivity
|
||||
@Inject
|
||||
constructor(
|
||||
private val falsingCollector: FalsingCollector,
|
||||
private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
|
||||
) : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.user_switcher_fullscreen)
|
||||
window.decorView.windowInsetsController?.let { controller ->
|
||||
controller.systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller.hide(Type.systemBars())
|
||||
}
|
||||
val viewModel =
|
||||
ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
|
||||
UserSwitcherViewBinder.bind(
|
||||
view = requireViewById(R.id.user_switcher_root),
|
||||
viewModel = viewModel,
|
||||
lifecycleOwner = this,
|
||||
layoutInflater = layoutInflater,
|
||||
falsingCollector = falsingCollector,
|
||||
onFinish = this::finish,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
import com.android.systemui.statusbar.phone.SystemUIDialog
|
||||
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
|
||||
class UserSwitchFullscreenDialog(
|
||||
context: Context,
|
||||
private val falsingCollector: FalsingCollector,
|
||||
private val userSwitcherViewModel: UserSwitcherViewModel,
|
||||
) : SystemUIDialog(context, R.style.Theme_UserSwitcherFullscreenDialog) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setShowForAllUsers(true)
|
||||
setCanceledOnTouchOutside(true)
|
||||
|
||||
window?.decorView?.windowInsetsController?.let { controller ->
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
}
|
||||
|
||||
val view =
|
||||
LayoutInflater.from(this.context).inflate(R.layout.user_switcher_fullscreen, null)
|
||||
setContentView(view)
|
||||
|
||||
UserSwitcherViewBinder.bind(
|
||||
view = requireViewById(R.id.user_switcher_root),
|
||||
viewModel = userSwitcherViewModel,
|
||||
layoutInflater = layoutInflater,
|
||||
falsingCollector = falsingCollector,
|
||||
onFinish = this::dismiss,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getWidth(): Int {
|
||||
val displayMetrics = context.resources.displayMetrics.apply {
|
||||
context.display.getRealMetrics(this)
|
||||
}
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
override fun getHeight() = MATCH_PARENT
|
||||
|
||||
}
|
||||
@@ -48,7 +48,6 @@ 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
|
||||
@@ -502,24 +501,12 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun showUserSwitcher(context: Context, expandable: Expandable) {
|
||||
if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
|
||||
fun showUserSwitcher(expandable: Expandable) {
|
||||
if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
|
||||
showDialog(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable))
|
||||
} else {
|
||||
showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(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,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDialog(request: ShowDialogRequestModel) {
|
||||
|
||||
@@ -50,4 +50,8 @@ sealed class ShowDialogRequestModel(
|
||||
data class ShowUserSwitcherDialog(
|
||||
override val expandable: Expandable?,
|
||||
) : ShowDialogRequestModel()
|
||||
|
||||
data class ShowUserSwitcherFullscreenDialog(
|
||||
override val expandable: Expandable?,
|
||||
) : ShowDialogRequestModel()
|
||||
}
|
||||
|
||||
@@ -31,19 +31,18 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.helper.widget.Flow as FlowWidget
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.systemui.Gefingerpoken
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
import com.android.systemui.lifecycle.repeatWhenAttached
|
||||
import com.android.systemui.user.UserSwitcherPopupMenu
|
||||
import com.android.systemui.user.UserSwitcherRootView
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
import com.android.systemui.util.children
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -56,7 +55,6 @@ object UserSwitcherViewBinder {
|
||||
fun bind(
|
||||
view: ViewGroup,
|
||||
viewModel: UserSwitcherViewModel,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
layoutInflater: LayoutInflater,
|
||||
falsingCollector: FalsingCollector,
|
||||
onFinish: () -> Unit,
|
||||
@@ -79,88 +77,92 @@ object UserSwitcherViewBinder {
|
||||
addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() }
|
||||
cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() }
|
||||
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
launch {
|
||||
viewModel.isFinishRequested
|
||||
.filter { it }
|
||||
.collect {
|
||||
onFinish()
|
||||
viewModel.onFinished()
|
||||
}
|
||||
view.repeatWhenAttached {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
launch {
|
||||
viewModel.isFinishRequested
|
||||
.filter { it }
|
||||
.collect {
|
||||
//finish requested, we want to dismiss popupmenu at the same time
|
||||
popupMenu?.dismiss()
|
||||
onFinish()
|
||||
viewModel.onFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } }
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } }
|
||||
|
||||
launch {
|
||||
viewModel.isMenuVisible.collect { isVisible ->
|
||||
if (isVisible && popupMenu?.isShowing != true) {
|
||||
popupMenu?.dismiss()
|
||||
// Use post to make sure we show the popup menu *after* the activity is
|
||||
// ready to show one to avoid a WindowManager$BadTokenException.
|
||||
view.post {
|
||||
popupMenu =
|
||||
createAndShowPopupMenu(
|
||||
context = view.context,
|
||||
anchorView = addButton,
|
||||
adapter = popupMenuAdapter,
|
||||
onDismissed = viewModel::onMenuClosed,
|
||||
)
|
||||
}
|
||||
} else if (!isVisible && popupMenu?.isShowing == true) {
|
||||
popupMenu?.dismiss()
|
||||
popupMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.menu.collect { menuViewModels ->
|
||||
popupMenuAdapter.setItems(menuViewModels)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.maximumUserColumns.collect { maximumColumns ->
|
||||
flowWidget.setMaxElementsWrap(maximumColumns)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.users.collect { users ->
|
||||
val viewPool =
|
||||
gridContainerView.children
|
||||
.filter { it.tag == USER_VIEW_TAG }
|
||||
.toMutableList()
|
||||
viewPool.forEach {
|
||||
gridContainerView.removeView(it)
|
||||
flowWidget.removeView(it)
|
||||
}
|
||||
users.forEach { userViewModel ->
|
||||
val userView =
|
||||
if (viewPool.isNotEmpty()) {
|
||||
viewPool.removeAt(0)
|
||||
} else {
|
||||
val inflatedView =
|
||||
layoutInflater.inflate(
|
||||
R.layout.user_switcher_fullscreen_item,
|
||||
view,
|
||||
false,
|
||||
launch {
|
||||
viewModel.isMenuVisible.collect { isVisible ->
|
||||
if (isVisible && popupMenu?.isShowing != true) {
|
||||
popupMenu?.dismiss()
|
||||
// Use post to make sure we show the popup menu *after* the activity is
|
||||
// ready to show one to avoid a WindowManager$BadTokenException.
|
||||
view.post {
|
||||
popupMenu =
|
||||
createAndShowPopupMenu(
|
||||
context = view.context,
|
||||
anchorView = addButton,
|
||||
adapter = popupMenuAdapter,
|
||||
onDismissed = viewModel::onMenuClosed,
|
||||
)
|
||||
inflatedView.tag = USER_VIEW_TAG
|
||||
inflatedView
|
||||
}
|
||||
userView.id = View.generateViewId()
|
||||
gridContainerView.addView(userView)
|
||||
flowWidget.addView(userView)
|
||||
UserViewBinder.bind(
|
||||
view = userView,
|
||||
viewModel = userViewModel,
|
||||
)
|
||||
} else if (!isVisible && popupMenu?.isShowing == true) {
|
||||
popupMenu?.dismiss()
|
||||
popupMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.menu.collect { menuViewModels ->
|
||||
popupMenuAdapter.setItems(menuViewModels)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.maximumUserColumns.collect { maximumColumns ->
|
||||
flowWidget.setMaxElementsWrap(maximumColumns)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.users.collect { users ->
|
||||
val viewPool =
|
||||
gridContainerView.children
|
||||
.filter { it.tag == USER_VIEW_TAG }
|
||||
.toMutableList()
|
||||
viewPool.forEach {
|
||||
gridContainerView.removeView(it)
|
||||
flowWidget.removeView(it)
|
||||
}
|
||||
users.forEach { userViewModel ->
|
||||
val userView =
|
||||
if (viewPool.isNotEmpty()) {
|
||||
viewPool.removeAt(0)
|
||||
} else {
|
||||
val inflatedView =
|
||||
layoutInflater.inflate(
|
||||
R.layout.user_switcher_fullscreen_item,
|
||||
view,
|
||||
false,
|
||||
)
|
||||
inflatedView.tag = USER_VIEW_TAG
|
||||
inflatedView
|
||||
}
|
||||
userView.id = View.generateViewId()
|
||||
gridContainerView.addView(userView)
|
||||
flowWidget.addView(userView)
|
||||
UserViewBinder.bind(
|
||||
view = userView,
|
||||
viewModel = userViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,16 @@ import com.android.systemui.CoreStartable
|
||||
import com.android.systemui.animation.DialogCuj
|
||||
import com.android.systemui.animation.DialogLaunchAnimator
|
||||
import com.android.systemui.broadcast.BroadcastSender
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
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.UserSwitchFullscreenDialog
|
||||
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||
import com.android.systemui.user.domain.model.ShowDialogRequestModel
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
import dagger.Lazy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -54,6 +57,8 @@ constructor(
|
||||
private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>,
|
||||
private val eventLogger: Lazy<UiEventLogger>,
|
||||
private val activityStarter: Lazy<ActivityStarter>,
|
||||
private val falsingCollector: Lazy<FalsingCollector>,
|
||||
private val userSwitcherViewModel: Lazy<UserSwitcherViewModel>,
|
||||
) : CoreStartable {
|
||||
|
||||
private var currentDialog: Dialog? = null
|
||||
@@ -124,6 +129,15 @@ constructor(
|
||||
INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
|
||||
),
|
||||
)
|
||||
is ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog ->
|
||||
Pair(
|
||||
UserSwitchFullscreenDialog(
|
||||
context = context.get(),
|
||||
falsingCollector = falsingCollector.get(),
|
||||
userSwitcherViewModel = userSwitcherViewModel.get(),
|
||||
),
|
||||
null, /* dialogCuj */
|
||||
)
|
||||
}
|
||||
currentDialog = dialog
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ constructor(
|
||||
interactor.selectedUser.mapLatest { userModel -> userModel.image }
|
||||
|
||||
/** Action to execute on click. Should launch the user switcher */
|
||||
val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) }
|
||||
val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(it) }
|
||||
}
|
||||
|
||||
@@ -17,12 +17,10 @@
|
||||
|
||||
package com.android.systemui.user.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.common.shared.model.Text
|
||||
import com.android.systemui.common.ui.drawable.CircularDrawable
|
||||
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.user.domain.interactor.GuestUserInteractor
|
||||
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
|
||||
@@ -36,12 +34,13 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** Models UI state for the user switcher feature. */
|
||||
@SysUISingleton
|
||||
class UserSwitcherViewModel
|
||||
private constructor(
|
||||
@Inject
|
||||
constructor(
|
||||
private val userInteractor: UserInteractor,
|
||||
private val guestUserInteractor: GuestUserInteractor,
|
||||
private val powerInteractor: PowerInteractor,
|
||||
) : ViewModel() {
|
||||
) {
|
||||
|
||||
/** On-device users. */
|
||||
val users: Flow<List<UserViewModel>> =
|
||||
@@ -112,34 +111,15 @@ private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFinishRequestedFlow(): Flow<Boolean> {
|
||||
var mostRecentSelectedUserId: Int? = null
|
||||
var mostRecentIsInteractive: Boolean? = null
|
||||
|
||||
return combine(
|
||||
// When the user is switched, we should finish.
|
||||
userInteractor.selectedUser
|
||||
.map { it.id }
|
||||
.map {
|
||||
val selectedUserChanged =
|
||||
mostRecentSelectedUserId != null && mostRecentSelectedUserId != it
|
||||
mostRecentSelectedUserId = it
|
||||
selectedUserChanged
|
||||
},
|
||||
// When the screen turns off, we should finish.
|
||||
powerInteractor.isInteractive.map {
|
||||
val screenTurnedOff = mostRecentIsInteractive == true && !it
|
||||
mostRecentIsInteractive = it
|
||||
screenTurnedOff
|
||||
},
|
||||
private fun createFinishRequestedFlow(): Flow<Boolean> =
|
||||
combine(
|
||||
// When the cancel button is clicked, we should finish.
|
||||
hasCancelButtonBeenClicked,
|
||||
// If an executed action told us to finish, we should finish,
|
||||
isFinishRequiredDueToExecutedAction,
|
||||
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
|
||||
selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
|
||||
) { cancelButtonClicked, executedActionFinish ->
|
||||
cancelButtonClicked || executedActionFinish
|
||||
}
|
||||
}
|
||||
|
||||
private fun toViewModel(
|
||||
model: UserModel,
|
||||
@@ -210,22 +190,4 @@ private constructor(
|
||||
{ userInteractor.selectUser(model.id) }
|
||||
}
|
||||
}
|
||||
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
private val userInteractor: UserInteractor,
|
||||
private val guestUserInteractor: GuestUserInteractor,
|
||||
private val powerInteractor: PowerInteractor,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return UserSwitcherViewModel(
|
||||
userInteractor = userInteractor,
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
powerInteractor = powerInteractor,
|
||||
)
|
||||
as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
@@ -49,7 +48,6 @@ import com.android.systemui.statusbar.CommandQueue
|
||||
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
|
||||
@@ -57,11 +55,9 @@ 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 junit.framework.Assert.assertNotNull
|
||||
@@ -800,7 +796,7 @@ class UserInteractorTest : SysuiTestCase() {
|
||||
fun `show user switcher - full screen disabled - shows dialog switcher`() =
|
||||
testScope.runTest {
|
||||
val expandable = mock<Expandable>()
|
||||
underTest.showUserSwitcher(context, expandable)
|
||||
underTest.showUserSwitcher(expandable)
|
||||
|
||||
val dialogRequest = collectLastValue(underTest.dialogShowRequests)
|
||||
|
||||
@@ -813,30 +809,22 @@ class UserInteractorTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show user switcher - full screen enabled - launches activity`() {
|
||||
featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
|
||||
fun `show user switcher - full screen enabled - launches full screen dialog`() =
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
|
||||
|
||||
val expandable = mock<Expandable>()
|
||||
underTest.showUserSwitcher(context, expandable)
|
||||
val expandable = mock<Expandable>()
|
||||
underTest.showUserSwitcher(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,
|
||||
)
|
||||
)
|
||||
}
|
||||
val dialogRequest = collectLastValue(underTest.dialogShowRequests)
|
||||
|
||||
// Dialog is shown.
|
||||
assertThat(dialogRequest())
|
||||
.isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable))
|
||||
|
||||
underTest.onDialogShown()
|
||||
assertThat(dialogRequest()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users - secondary user - managed profile is not included`() =
|
||||
|
||||
@@ -34,8 +34,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerReposito
|
||||
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.power.data.repository.FakePowerRepository
|
||||
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
|
||||
@@ -88,7 +86,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var userRepository: FakeUserRepository
|
||||
private lateinit var keyguardRepository: FakeKeyguardRepository
|
||||
private lateinit var powerRepository: FakePowerRepository
|
||||
|
||||
private lateinit var testDispatcher: TestDispatcher
|
||||
private lateinit var testScope: TestScope
|
||||
@@ -116,7 +113,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
keyguardRepository = FakeKeyguardRepository()
|
||||
powerRepository = FakePowerRepository()
|
||||
val refreshUsersScheduler =
|
||||
RefreshUsersScheduler(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
@@ -145,7 +141,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
set(Flags.FACE_AUTH_REFACTOR, true)
|
||||
}
|
||||
underTest =
|
||||
UserSwitcherViewModel.Factory(
|
||||
UserSwitcherViewModel(
|
||||
userInteractor =
|
||||
UserInteractor(
|
||||
applicationContext = context,
|
||||
@@ -173,13 +169,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
refreshUsersScheduler = refreshUsersScheduler,
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
),
|
||||
powerInteractor =
|
||||
PowerInteractor(
|
||||
repository = powerRepository,
|
||||
),
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
)
|
||||
.create(UserSwitcherViewModel::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -325,47 +316,13 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isFinishRequested - finishes when user is switched`() =
|
||||
testScope.runTest {
|
||||
val userInfos = setUsers(count = 2)
|
||||
val isFinishRequested = mutableListOf<Boolean>()
|
||||
val job =
|
||||
launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
|
||||
assertThat(isFinishRequested.last()).isFalse()
|
||||
|
||||
userRepository.setSelectedUserInfo(userInfos[1])
|
||||
|
||||
assertThat(isFinishRequested.last()).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isFinishRequested - finishes when the screen turns off`() =
|
||||
testScope.runTest {
|
||||
setUsers(count = 2)
|
||||
powerRepository.setInteractive(true)
|
||||
val isFinishRequested = mutableListOf<Boolean>()
|
||||
val job =
|
||||
launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
|
||||
assertThat(isFinishRequested.last()).isFalse()
|
||||
|
||||
powerRepository.setInteractive(false)
|
||||
|
||||
assertThat(isFinishRequested.last()).isTrue()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isFinishRequested - finishes when cancel button is clicked`() =
|
||||
testScope.runTest {
|
||||
setUsers(count = 2)
|
||||
powerRepository.setInteractive(true)
|
||||
val isFinishRequested = mutableListOf<Boolean>()
|
||||
val job =
|
||||
launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
|
||||
launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
|
||||
assertThat(isFinishRequested.last()).isFalse()
|
||||
|
||||
underTest.onCancelButtonClicked()
|
||||
|
||||
Reference in New Issue
Block a user