From ea92f14c6a4095870f09baf8e28a3ce013182acd Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 29 Sep 2022 17:40:27 -0700 Subject: [PATCH] UserRecord support in user interactor. This is needed for backwards-compatibility purposes. Classes that extend BaseUserSwitcherAdapter deal in UserRecord data items and are too numerous and too risky to refactor to use the newer UserModel and UserActionModel. As part of this work, this CL extracts shared logic to instantiate UserRecord instances from UserSwitcherControllerOldImpl so it can be reused by UserInteractor. Bug: 246631653 Test: unit tests included. Manually verified old implementation still works across status bar, quick settings, bouncer dropdown, and full-screen user switcher. Change-Id: I2214b26700a539551cae84eb71b7b90d2e785bef --- .../policy/UserSwitcherControllerOldImpl.java | 119 ++++++--------- .../user/domain/interactor/UserInteractor.kt | 91 ++++++++++++ .../legacyhelper/data/LegacyUserDataHelper.kt | 135 ++++++++++++++++++ .../UserInteractorRefactoredTest.kt | 108 +++++++++++++- .../domain/interactor/UserInteractorTest.kt | 9 +- 5 files changed, 377 insertions(+), 85 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java index 9cfe7a7e48cea..46d2f3ac9ce4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.policy; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.annotation.UserIdInt; import android.app.AlertDialog; import android.app.Dialog; @@ -51,11 +49,9 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.LatencyTracker; -import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.users.UserCreatingDialog; import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.R; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; @@ -73,6 +69,8 @@ import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.data.source.UserRecord; +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; +import com.android.systemui.user.shared.model.UserActionModel; import com.android.systemui.user.ui.dialog.AddUserDialog; import com.android.systemui.user.ui.dialog.ExitGuestDialog; import com.android.systemui.util.settings.GlobalSettings; @@ -327,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { for (UserInfo info : infos) { boolean isCurrent = currentId == info.id; - boolean switchToEnabled = canSwitchUsers || isCurrent; if (!mUserSwitcherEnabled && !info.isPrimary()) { continue; } @@ -336,26 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (info.isGuest()) { // Tapping guest icon triggers remove and a user switch therefore // the icon shouldn't be enabled even if the user is current - guestRecord = new UserRecord(info, null /* picture */, - true /* isGuest */, isCurrent, false /* isAddUser */, - false /* isRestricted */, canSwitchUsers, - false /* isAddSupervisedUser */, null /* enforcedAdmin */); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + null /* picture */, + info, + isCurrent, + canSwitchUsers); } else if (info.supportsSwitchToByUser()) { - Bitmap picture = bitmaps.get(info.id); - if (picture == null) { - picture = mUserManager.getUserIcon(info.id); - - if (picture != null) { - int avatarSize = mContext.getResources() - .getDimensionPixelSize(R.dimen.max_avatar_size); - picture = Bitmap.createScaledBitmap( - picture, avatarSize, avatarSize, true); - } - } - records.add(new UserRecord(info, picture, false /* isGuest */, - isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled, false /* isAddSupervisedUser */, - null /* enforcedAdmin */)); + records.add( + LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + bitmaps.get(info.id), + info, + isCurrent, + canSwitchUsers)); } } } @@ -366,28 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { // we will just use it as an indicator for "Resetting guest...". // Otherwise, default to canSwitchUsers. boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = new UserRecord( - null /* info */, - null /* picture */, - true /* isGuest */, - false /* isCurrent */, - false /* isAddUser */, + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, false /* isRestricted */, - isSwitchToGuestEnabled, - false /* isAddSupervisedUser */, - getEnforcedAdmin()); + isSwitchToGuestEnabled); records.add(guestRecord); } else if (canCreateGuest(guestRecord != null)) { - guestRecord = new UserRecord( - null /* info */, - null /* picture */, - true /* isGuest */, - false /* isCurrent */, - false /* isAddUser */, - createIsRestricted(), - canSwitchUsers, - false /* isAddSupervisedUser */, - getEnforcedAdmin()); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + canSwitchUsers); records.add(guestRecord); } } else { @@ -395,31 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } if (canCreateUser()) { - UserRecord addUserRecord = new UserRecord( - null /* info */, - null /* picture */, - false /* isGuest */, - false /* isCurrent */, - true /* isAddUser */, + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_USER, createIsRestricted(), - canSwitchUsers, - false /* isAddSupervisedUser */, - getEnforcedAdmin()); - records.add(addUserRecord); + canSwitchUsers); + records.add(userRecord); } if (canCreateSupervisedUser()) { - UserRecord addUserRecord = new UserRecord( - null /* info */, - null /* picture */, - false /* isGuest */, - false /* isCurrent */, - false /* isAddUser */, + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_SUPERVISED_USER, createIsRestricted(), - canSwitchUsers, - true /* isAddSupervisedUser */, - getEnforcedAdmin()); - records.add(addUserRecord); + canSwitchUsers); + records.add(userRecord); } mUiExecutor.execute(() -> { @@ -998,22 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { return mKeyguardStateController.isShowing(); } - @Nullable - private EnforcedAdmin getEnforcedAdmin() { - final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( - mContext, - UserManager.DISALLOW_ADD_USER, - mUserTracker.getUserId()); - if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction( - mContext, - UserManager.DISALLOW_ADD_USER, - mUserTracker.getUserId())) { - return admin; - } else { - return null; - } - } - private boolean shouldUseSimpleUserSwitcher() { int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; 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 b923850e4cbfe..7d2d1224fc370 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 @@ -46,7 +46,9 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel import com.android.systemui.util.kotlin.pairwise @@ -57,6 +59,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -65,6 +69,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @@ -234,6 +239,54 @@ constructor( } } + val userRecords: StateFlow> = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { + toRecord( + action = it, + selectedUserId = selectedUserInfo.id, + isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen, + ) + } + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) + } else { + MutableStateFlow(ArrayList()) + } + + val selectedUserRecord: StateFlow = + if (isNewImpl) { + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + } else { + MutableStateFlow(null) + } + /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated @@ -407,6 +460,44 @@ constructor( } } + private suspend fun toRecord( + userInfo: UserInfo, + selectedUserId: Int, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + manager = manager, + userInfo = userInfo, + picture = null, + isCurrent = userInfo.id == selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + ) + } + + private suspend fun toRecord( + action: UserActionModel, + selectedUserId: Int, + isAddFromLockscreenEnabled: Boolean, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + selectedUserId = selectedUserId, + actionType = action, + isRestricted = + if (action == UserActionModel.ENTER_GUEST_MODE) { + // Entering guest mode is never restricted, so it's allowed to happen from the + // lockscreen even if the "add from lockscreen" system setting is off. + false + } else { + !isAddFromLockscreenEnabled + }, + isSwitchToEnabled = + canSwitchUsers(selectedUserId) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting), + ) + } + private fun exitGuestUser( @UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int, diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt new file mode 100644 index 0000000000000..8f6662f7b192f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt @@ -0,0 +1,135 @@ +/* + * 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.legacyhelper.data + +import android.content.Context +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.os.UserManager +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.systemui.R +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel + +/** + * Defines utility functions for helping with legacy data code for users. + * + * We need these to avoid code duplication between logic inside the UserSwitcherController and in + * modern architecture classes such as repositories, interactors, and view-models. If we ever + * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites. + */ +object LegacyUserDataHelper { + + @JvmStatic + fun createRecord( + context: Context, + manager: UserManager, + picture: Bitmap?, + userInfo: UserInfo, + isCurrent: Boolean, + canSwitchUsers: Boolean, + ): UserRecord { + val isGuest = userInfo.isGuest + return UserRecord( + info = userInfo, + picture = + getPicture( + manager = manager, + context = context, + userInfo = userInfo, + picture = picture, + ), + isGuest = isGuest, + isCurrent = isCurrent, + isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest), + ) + } + + @JvmStatic + fun createRecord( + context: Context, + selectedUserId: Int, + actionType: UserActionModel, + isRestricted: Boolean, + isSwitchToEnabled: Boolean, + ): UserRecord { + return UserRecord( + isGuest = actionType == UserActionModel.ENTER_GUEST_MODE, + isAddUser = actionType == UserActionModel.ADD_USER, + isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER, + isRestricted = isRestricted, + isSwitchToEnabled = isSwitchToEnabled, + enforcedAdmin = + getEnforcedAdmin( + context = context, + selectedUserId = selectedUserId, + ), + ) + } + + private fun getEnforcedAdmin( + context: Context, + selectedUserId: Int, + ): EnforcedAdmin? { + val admin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ?: return null + + return if ( + !RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ) { + admin + } else { + null + } + } + + private fun getPicture( + context: Context, + manager: UserManager, + userInfo: UserInfo, + picture: Bitmap?, + ): Bitmap? { + if (userInfo.isGuest) { + return null + } + + if (picture != null) { + return picture + } + + val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null + + val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size) + return Bitmap.createScaledBitmap( + unscaledOrNull, + avatarSize, + avatarSize, + /* filter= */ true, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt index 68d710560ebfc..3d5695a09ebca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -29,6 +29,7 @@ import com.android.internal.R.drawable.ic_account_circle import com.android.systemui.R import com.android.systemui.common.shared.model.Text import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.model.ShowDialogRequestModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel @@ -62,6 +63,7 @@ class UserInteractorRefactoredTest : UserInteractorTest() { super.setUp() overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) overrideResource( com.android.internal.R.string.config_supervisedUserCreationPackage, SUPERVISED_USER_CREATION_APP_PACKAGE, @@ -470,6 +472,49 @@ class UserInteractorRefactoredTest : UserInteractorTest() { assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) } + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + private fun assertUsers( models: List?, count: Int, @@ -502,6 +547,65 @@ class UserInteractorRefactoredTest : UserInteractorTest() { assertThat(model.isGuest).isEqualTo(isGuest) } + private fun assertRecords( + records: List, + userIds: List, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + private fun assertRecordForAction( + record: UserRecord, + type: UserActionModel, + ) { + assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) + assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) + assertThat(record.isAddSupervisedUser) + .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + } + private fun createUserInfos( count: Int, includeGuest: Boolean, @@ -547,8 +651,8 @@ class UserInteractorRefactoredTest : UserInteractorTest() { companion object { private val IMMEDIATE = Dispatchers.Main.immediate - private val ICON: Bitmap = mock() + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) private val GUEST_ICON: Drawable = mock() - private val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" } } 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 cdfe9208e0b82..d57f84b5704a6 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 @@ -49,6 +49,7 @@ abstract class UserInteractorTest : SysuiTestCase() { protected lateinit var underTest: UserInteractor + protected lateinit var testCoroutineScope: TestCoroutineScope protected lateinit var userRepository: FakeUserRepository protected lateinit var keyguardRepository: FakeKeyguardRepository protected lateinit var telephonyRepository: FakeTelephonyRepository @@ -61,10 +62,10 @@ abstract class UserInteractorTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() telephonyRepository = FakeTelephonyRepository() - val applicationScope = TestCoroutineScope() + testCoroutineScope = TestCoroutineScope() val refreshUsersScheduler = RefreshUsersScheduler( - applicationScope = applicationScope, + applicationScope = testCoroutineScope, mainDispatcher = IMMEDIATE, repository = userRepository, ) @@ -83,7 +84,7 @@ abstract class UserInteractorTest : SysuiTestCase() { set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored()) }, manager = manager, - applicationScope = applicationScope, + applicationScope = testCoroutineScope, telephonyInteractor = TelephonyInteractor( repository = telephonyRepository, @@ -95,7 +96,7 @@ abstract class UserInteractorTest : SysuiTestCase() { guestUserInteractor = GuestUserInteractor( applicationContext = context, - applicationScope = applicationScope, + applicationScope = testCoroutineScope, mainDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE, manager = manager,