From 963f710d4c04febb860ae9daf06a4cea44b4720f Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 19 Sep 2022 09:55:51 -0700 Subject: Moves enforcedAdmin into UserRecord. In an effort to move logic out of the old UserSwitcherController implementation, we are moving the concept of an enforced admin for user actions that are disabled by an administrator into the actual UserRecord itself. This CL has no logical changes beyond moving where the state is being held to a more common one. Bug: 246631653 Test: Manually verified the old implementation of the full-screen user switcher, the quick settings button, and the lock-screen user dropdown all still work properly. Change-Id: I8b6bd8f3424fe531aa4889f530968f6f48fec536 --- .../android/systemui/qs/tiles/UserDetailView.java | 8 +- .../policy/KeyguardUserSwitcherController.java | 2 +- .../statusbar/policy/UserSwitcherController.kt | 7 -- .../statusbar/policy/UserSwitcherControllerImpl.kt | 17 ---- .../policy/UserSwitcherControllerOldImpl.java | 99 ++++++++++++---------- .../systemui/user/data/source/UserRecord.kt | 14 +++ .../keyguard/KeyguardSecurityContainerTest.java | 2 +- 7 files changed, 74 insertions(+), 75 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 97476b2d1cde..d2d5063c7ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView { v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(mController.isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); @@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView { Trace.beginSection("UserDetailView.Adapter#onClick"); UserRecord userRecord = (UserRecord) view.getTag(); - if (mController.isDisabledByAdmin(userRecord)) { + if (userRecord.isDisabledByAdmin()) { final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( - mContext, mController.getEnforcedAdmin(userRecord)); + mContext, userRecord.enforcedAdmin); mController.startActivity(intent); } else if (userRecord.isSwitchToEnabled) { MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); if (!userRecord.isAddUser && !userRecord.isRestricted - && !mController.isDisabledByAdmin(userRecord)) { + && !userRecord.isDisabledByAdmin()) { if (mCurrentUserView != null) { mCurrentUserView.setActivated(false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java index 0995a00533a8..712953e14d60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java @@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController mEnforcedAdminByUserRecord = - new SimpleArrayMap<>(); - private final ArraySet mDisabledByAdmin = new ArraySet<>(); private ArrayList mUsers = new ArrayList<>(); @VisibleForTesting @@ -346,7 +341,7 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { guestRecord = new UserRecord(info, null /* picture */, true /* isGuest */, isCurrent, false /* isAddUser */, false /* isRestricted */, canSwitchUsers, - false /* isAddSupervisedUser */); + false /* isAddSupervisedUser */, null /* enforcedAdmin */); } else if (info.supportsSwitchToByUser()) { Bitmap picture = bitmaps.get(info.id); if (picture == null) { @@ -361,7 +356,8 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } records.add(new UserRecord(info, picture, false /* isGuest */, isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled, false /* isAddSupervisedUser */)); + switchToEnabled, false /* isAddSupervisedUser */, + null /* enforcedAdmin */)); } } } @@ -372,18 +368,28 @@ 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 */, false /* isRestricted */, - isSwitchToGuestEnabled, false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = new UserRecord( + null /* info */, + null /* picture */, + true /* isGuest */, + false /* isCurrent */, + false /* isAddUser */, + false /* isRestricted */, + isSwitchToGuestEnabled, + false /* isAddSupervisedUser */, + getEnforcedAdmin()); 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 */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = new UserRecord( + null /* info */, + null /* picture */, + true /* isGuest */, + false /* isCurrent */, + false /* isAddUser */, + createIsRestricted(), + canSwitchUsers, + false /* isAddSupervisedUser */, + getEnforcedAdmin()); records.add(guestRecord); } } else { @@ -391,19 +397,30 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } if (canCreateUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, true /* isAddUser */, - createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); + UserRecord addUserRecord = new UserRecord( + null /* info */, + null /* picture */, + false /* isGuest */, + false /* isCurrent */, + true /* isAddUser */, + createIsRestricted(), + canSwitchUsers, + false /* isAddSupervisedUser */, + getEnforcedAdmin()); records.add(addUserRecord); } if (canCreateSupervisedUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, false /* isAddUser */, - createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); + UserRecord addUserRecord = new UserRecord( + null /* info */, + null /* picture */, + false /* isGuest */, + false /* isCurrent */, + false /* isAddUser */, + createIsRestricted(), + canSwitchUsers, + true /* isAddSupervisedUser */, + getEnforcedAdmin()); records.add(addUserRecord); } @@ -964,27 +981,19 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { return mKeyguardStateController.isShowing(); } - @Override @Nullable - public EnforcedAdmin getEnforcedAdmin(UserRecord record) { - return mEnforcedAdminByUserRecord.get(record); - } - - @Override - public boolean isDisabledByAdmin(UserRecord record) { - return mDisabledByAdmin.contains(record); - } - - private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) { - EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId()); - if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) { - mDisabledByAdmin.add(record); - mEnforcedAdminByUserRecord.put(record, admin); + 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 { - mDisabledByAdmin.remove(record); - mEnforcedAdminByUserRecord.put(record, null); + return null; } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index cf6da9a60d78..9370286d7ee7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.data.source import android.content.pm.UserInfo import android.graphics.Bitmap import android.os.UserHandle +import com.android.settingslib.RestrictedLockUtils /** Encapsulates raw data for a user or an option item related to managing users on the device. */ data class UserRecord( @@ -41,6 +42,11 @@ data class UserRecord( @JvmField val isSwitchToEnabled: Boolean = false, /** Whether this record represents an option to add another supervised user to the device. */ @JvmField val isAddSupervisedUser: Boolean = false, + /** + * An enforcing admin, if the user action represented by this record is disabled by the admin. + * If not disabled, this is `null`. + */ + @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null, ) { /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */ fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { @@ -59,6 +65,14 @@ data class UserRecord( } } + /** + * Returns `true` if the user action represented by this record has been disabled by an admin; + * `false` otherwise. + */ + fun isDisabledByAdmin(): Boolean { + return enforcedAdmin != null + } + companion object { @JvmStatic fun createForGuest(): UserRecord { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 43f6f1aac097..c1036e356cfa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */)); + false /* isAddSupervisedUser */, null /* enforcedAdmin */)); } return users; } -- cgit v1.2.3-59-g8ed1b From cd00a5dbc8326b5c15295cde4f65b9c18ade2597 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 21 Sep 2022 18:22:10 -0700 Subject: Adds telephony repository and interactor. This is needed for downstream dependencies to observe the call state. Bug: 246631653 Test: Unit test included Change-Id: If7f213dea82581e04620189ca3b13dea3295d9ae --- .../android/systemui/dagger/SystemUIModule.java | 2 + .../data/repository/TelephonyRepository.kt | 56 +++++++++++++++ .../data/repository/TelephonyRepositoryModule.kt | 26 +++++++ .../domain/interactor/TelephonyInteractor.kt | 34 +++++++++ .../data/repository/TelephonyRepositoryImplTest.kt | 82 ++++++++++++++++++++++ .../data/repository/FakeTelephonyRepository.kt | 32 +++++++++ 6 files changed, 232 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 443d2774f0e0..06dbab980793 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; +import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.user.UserModule; @@ -145,6 +146,7 @@ import dagger.Provides; StatusBarWindowModule.class, SysUIConcurrencyModule.class, SysUIUnfoldModule.class, + TelephonyRepositoryModule.class, TunerModule.class, UserModule.class, UtilModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt new file mode 100644 index 000000000000..9c38dc0f8852 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -0,0 +1,56 @@ +/* + * 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.telephony.data.repository + +import android.telephony.Annotation +import android.telephony.TelephonyCallback +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.TelephonyListenerManager +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that encapsulate _some_ telephony-related state. */ +interface TelephonyRepository { + /** The state of the current call. */ + @Annotation.CallState val callState: Flow +} + +/** + * NOTE: This repository tracks only telephony-related state regarding the default mobile + * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a + * per-subscription basis and thus will always be tracking telephony information regarding + * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager` + * for more documentation. + */ +@SysUISingleton +class TelephonyRepositoryImpl +@Inject +constructor( + private val manager: TelephonyListenerManager, +) : TelephonyRepository { + @Annotation.CallState + override val callState: Flow = conflatedCallbackFlow { + val listener = TelephonyCallback.CallStateListener { state -> trySend(state) } + + manager.addCallStateListener(listener) + + awaitClose { manager.removeCallStateListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt new file mode 100644 index 000000000000..630fbf2d1a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * 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.telephony.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface TelephonyRepositoryModule { + @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt new file mode 100644 index 000000000000..86ca33df24dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -0,0 +1,34 @@ +/* + * 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.telephony.domain.interactor + +import android.telephony.Annotation +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.data.repository.TelephonyRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Hosts business logic related to telephony. */ +@SysUISingleton +class TelephonyInteractor +@Inject +constructor( + repository: TelephonyRepository, +) { + @Annotation.CallState val callState: Flow = repository.callState +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt new file mode 100644 index 000000000000..773a0d8ceb64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * 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.telephony.data.repository + +import android.telephony.TelephonyCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class TelephonyRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var manager: TelephonyListenerManager + + private lateinit var underTest: TelephonyRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + TelephonyRepositoryImpl( + manager = manager, + ) + } + + @Test + fun callState() = + runBlocking(IMMEDIATE) { + var callState: Int? = null + val job = underTest.callState.onEach { callState = it }.launchIn(this) + val listenerCaptor = kotlinArgumentCaptor() + verify(manager).addCallStateListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + + listener.onCallStateChanged(0) + assertThat(callState).isEqualTo(0) + + listener.onCallStateChanged(1) + assertThat(callState).isEqualTo(1) + + listener.onCallStateChanged(2) + assertThat(callState).isEqualTo(2) + + job.cancel() + + verify(manager).removeCallStateListener(listener) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt new file mode 100644 index 000000000000..59f24ef2a706 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt @@ -0,0 +1,32 @@ +/* + * 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.telephony.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTelephonyRepository : TelephonyRepository { + + private val _callState = MutableStateFlow(0) + override val callState: Flow = _callState.asStateFlow() + + fun setCallState(value: Int) { + _callState.value = value + } +} -- cgit v1.2.3-59-g8ed1b From 0271aa49441c5297286c0f14b4aca741a46e582f Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Fri, 23 Sep 2022 16:53:09 -0700 Subject: Adds snapshot function for isKeyguardShowing. This complements the already-existing flow version of the same property and is needed by downstream CLs on this chain. Bug: 246631653 Test: test updated Change-Id: Ie37246d4b0614e477338dbcaad7e6aab0b7b0fa4 --- .../keyguard/data/repository/KeyguardRepository.kt | 15 ++++++++++++++- .../keyguard/domain/interactor/KeyguardInteractor.kt | 6 +++++- .../data/repository/KeyguardRepositoryImplTest.kt | 3 +++ .../keyguard/data/repository/FakeKeyguardRepository.kt | 4 ++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 840a4b20a3f0..4c4b588888d1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -85,6 +85,15 @@ interface KeyguardRepository { */ val dozeAmount: Flow + /** + * Returns `true` if the keyguard is showing; `false` otherwise. + * + * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in + * the z-order (which is not really above the system UI window, but rather - the lock-screen + * becomes invisible to reveal the "occluding activity"). + */ + fun isKeyguardShowing(): Boolean + /** Sets whether the bottom area UI should animate the transition out of doze state. */ fun setAnimateDozingTransitions(animate: Boolean) @@ -103,7 +112,7 @@ class KeyguardRepositoryImpl @Inject constructor( statusBarStateController: StatusBarStateController, - keyguardStateController: KeyguardStateController, + private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) @@ -168,6 +177,10 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } + override fun isKeyguardShowing(): Boolean { + return keyguardStateController.isShowing + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index dccc94178ed5..192919e32cf6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow class KeyguardInteractor @Inject constructor( - repository: KeyguardRepository, + private val repository: KeyguardRepository, ) { /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at @@ -40,4 +40,8 @@ constructor( val isDozing: Flow = repository.isDozing /** Whether the keyguard is showing ot not. */ val isKeyguardShowing: Flow = repository.isKeyguardShowing + + fun isKeyguardShowing(): Boolean { + return repository.isKeyguardShowing() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index ba1e168bc316..eea2e952c81f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this) assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() val captor = argumentCaptor() verify(keyguardStateController).addCallback(captor.capture()) @@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { whenever(keyguardStateController.isShowing).thenReturn(true) captor.value.onKeyguardShowingChanged() assertThat(latest).isTrue() + assertThat(underTest.isKeyguardShowing()).isTrue() whenever(keyguardStateController.isShowing).thenReturn(false) captor.value.onKeyguardShowingChanged() assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() job.cancel() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 42b434a9deaf..725b1f41372c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow = _dozeAmount + override fun isKeyguardShowing(): Boolean { + return _isKeyguardShowing.value + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.tryEmit(animate) } -- cgit v1.2.3-59-g8ed1b From 59ad257ef316468d646a944f8e5bf5da65c0d5b5 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 27 Sep 2022 17:44:41 -0700 Subject: Extracts dialogs out of old UserSwitcherController. In later CLs, we need to access the same dialog classes from our interactor. Bug: 246631653 Test: verified manually with followup CLs Change-Id: I6962b902f5b75302ff9779f9d95bbd76038cc79c --- .../policy/UserSwitcherControllerOldImpl.java | 162 ++++----------------- .../systemui/user/ui/dialog/AddUserDialog.kt | 107 ++++++++++++++ .../systemui/user/ui/dialog/ExitGuestDialog.kt | 132 +++++++++++++++++ 3 files changed, 264 insertions(+), 137 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.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 938fa880bbef..9cfe7a7e48ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java @@ -20,14 +20,12 @@ import static android.os.UserManager.SWITCHABILITY_STATUS_OK; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; @@ -73,10 +71,10 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.CreateUserActivity; import com.android.systemui.user.data.source.UserRecord; +import com.android.systemui.user.ui.dialog.AddUserDialog; +import com.android.systemui.user.ui.dialog.ExitGuestDialog; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -608,12 +606,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); } - private void showExitGuestDialog(int id, boolean isGuestEphemeral, - int targetId, DialogShower dialogShower) { + private void showExitGuestDialog( + int id, + boolean isGuestEphemeral, + int targetId, + DialogShower dialogShower) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); } - mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId); + mExitGuestDialog = new ExitGuestDialog( + mContext, + id, + isGuestEphemeral, + targetId, + mKeyguardStateController.isShowing(), + mFalsingManager, + mDialogLaunchAnimator, + this::exitGuestUser); if (dialogShower != null) { dialogShower.showDialog(mExitGuestDialog, new DialogCuj( InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, @@ -639,7 +648,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (mAddUserDialog != null && mAddUserDialog.isShowing()) { mAddUserDialog.cancel(); } - mAddUserDialog = new AddUserDialog(mContext); + final UserInfo currentUser = mUserTracker.getUserInfo(); + mAddUserDialog = new AddUserDialog( + mContext, + currentUser.getUserHandle(), + mKeyguardStateController.isShowing(), + /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), + mFalsingManager, + mBroadcastSender, + mDialogLaunchAnimator); if (dialogShower != null) { dialogShower.showDialog(mAddUserDialog, new DialogCuj( @@ -1061,133 +1078,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } } }; - - - private final class ExitGuestDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - private final int mGuestId; - private final int mTargetId; - private final boolean mIsGuestEphemeral; - - ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral, - int targetId) { - super(context); - if (isGuestEphemeral) { - setTitle(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_title)); - setMessage(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_message)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_dialog_button), this); - } else { - setTitle(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_title_non_ephemeral)); - setMessage(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_message_non_ephemeral)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_clear_data_button), - this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_save_data_button), - this); - } - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - setCanceledOnTouchOutside(false); - mGuestId = guestId; - mTargetId = targetId; - mIsGuestEphemeral = isGuestEphemeral; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.HIGH_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (mIsGuestEphemeral) { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Ephemeral guest: exit guest, guest is removed by the system - // on exit, since its marked ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - // Cancel clicked, do nothing - cancel(); - } - } else { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: exit guest, guest is not removed by the system - // on exit, since its marked non-ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: remove guest and then exit - exitGuestUser(mGuestId, mTargetId, true); - } else if (which == DialogInterface.BUTTON_NEUTRAL) { - // Cancel clicked, do nothing - cancel(); - } - } - } - } - - @VisibleForTesting - final class AddUserDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - AddUserDialog(Context context) { - super(context); - - setTitle(com.android.settingslib.R.string.user_add_user_title); - String message = context.getString( - com.android.settingslib.R.string.user_add_user_message_short); - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) { - message += context.getString(R.string.user_add_user_message_guest_remove); - } - setMessage(message); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString(android.R.string.ok), this); - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.MODERATE_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (which == BUTTON_NEUTRAL) { - cancel(); - } else { - mDialogLaunchAnimator.dismissStack(this); - if (ActivityManager.isUserAMonkey()) { - return; - } - // Use broadcast instead of ShadeController, as this dialog may have started in - // another process and normal dagger bindings are not available - mBroadcastSender.sendBroadcastAsUser( - new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT); - getContext().startActivityAsUser( - CreateUserActivity.createIntentForStart(getContext()), - mUserTracker.getUserHandle()); - } - } - } - } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt new file mode 100644 index 000000000000..a9d66de118e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt @@ -0,0 +1,107 @@ +/* + * 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.dialog + +import android.app.ActivityManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.UserHandle +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.CreateUserActivity + +/** Dialog for adding a new user to the device. */ +class AddUserDialog( + context: Context, + userHandle: UserHandle, + isKeyguardShowing: Boolean, + showEphemeralMessage: Boolean, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator +) : SystemUIDialog(context) { + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (which == BUTTON_NEUTRAL) { + cancel() + return + } + + dialogLaunchAnimator.dismissStack(this@AddUserDialog) + if (ActivityManager.isUserAMonkey()) { + return + } + + // Use broadcast instead of ShadeController, as this dialog may have started in + // another + // process where normal dagger bindings are not available. + broadcastSender.sendBroadcastAsUser( + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), + UserHandle.CURRENT + ) + + context.startActivityAsUser( + CreateUserActivity.createIntentForStart(context), + userHandle, + ) + } + } + + init { + setTitle(R.string.user_add_user_title) + val message = + context.getString(R.string.user_add_user_message_short) + + if (showEphemeralMessage) { + context.getString( + com.android.systemui.R.string.user_add_user_message_guest_remove + ) + } else { + "" + } + setMessage(message) + + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + + setButton( + BUTTON_POSITIVE, + context.getString(android.R.string.ok), + onClickListener, + ) + + setWindowOnTop(this, isKeyguardShowing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt new file mode 100644 index 000000000000..19ad44d8649f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt @@ -0,0 +1,132 @@ +/* + * 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.dialog + +import android.annotation.UserIdInt +import android.content.Context +import android.content.DialogInterface +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** Dialog for exiting the guest user. */ +class ExitGuestDialog( + context: Context, + private val guestUserId: Int, + private val isGuestEphemeral: Boolean, + private val targetUserId: Int, + isKeyguardShowing: Boolean, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val onExitGuestUserListener: OnExitGuestUserListener, +) : SystemUIDialog(context) { + + fun interface OnExitGuestUserListener { + fun onExitGuestUser( + @UserIdInt guestId: Int, + @UserIdInt targetId: Int, + forceRemoveGuest: Boolean, + ) + } + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (isGuestEphemeral) { + if (which == BUTTON_POSITIVE) { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Ephemeral guest: exit guest, guest is removed by the system + // on exit, since its marked ephemeral + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false) + } else if (which == BUTTON_NEGATIVE) { + // Cancel clicked, do nothing + cancel() + } + } else { + when (which) { + BUTTON_POSITIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: exit guest, guest is not removed by the system + // on exit, since its marked non-ephemeral + onExitGuestUserListener.onExitGuestUser( + guestUserId, + targetUserId, + false + ) + } + BUTTON_NEGATIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: remove guest and then exit + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true) + } + BUTTON_NEUTRAL -> { + // Cancel clicked, do nothing + cancel() + } + } + } + } + } + + init { + if (isGuestEphemeral) { + setTitle(context.getString(R.string.guest_exit_dialog_title)) + setMessage(context.getString(R.string.guest_exit_dialog_message)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_dialog_button), + onClickListener, + ) + } else { + setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral)) + setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_NEGATIVE, + context.getString(R.string.guest_exit_clear_data_button), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_save_data_button), + onClickListener, + ) + } + setWindowOnTop(this, isKeyguardShowing) + setCanceledOnTouchOutside(false) + } +} -- cgit v1.2.3-59-g8ed1b From 32b5486aa6c32f37e882f3e3030e2f965c121aba Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 27 Sep 2022 17:59:26 -0700 Subject: User data layer without dependency on USC Behind a flag, removes the dependency on UserSwitcherController from the data layer of the user feature. Bug: 246631653 Test: included tests, manually verified against the tip of the CL chain Change-Id: I0594dcfd5cc38d0ae09f620e01d0caafc1b956e3 --- .../user/data/model/UserSwitcherSettingsModel.kt | 25 +++ .../user/data/repository/UserRepository.kt | 224 ++++++++++++++++++++- .../systemui/user/shared/model/UserModel.kt | 3 + .../systemui/util/settings/SettingsProxyExt.kt | 48 +++++ .../repository/UserRepositoryImplRefactoredTest.kt | 204 +++++++++++++++++++ .../user/data/repository/UserRepositoryImplTest.kt | 209 +++---------------- .../UserRepositoryImplUnrefactoredTest.kt | 205 +++++++++++++++++++ .../android/systemui/settings/FakeUserTracker.kt | 29 ++- .../user/data/repository/FakeUserRepository.kt | 69 ++++++- 9 files changed, 817 insertions(+), 199 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt new file mode 100644 index 000000000000..4fd55c0e21c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt @@ -0,0 +1,25 @@ +/* + * 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.data.model + +/** Encapsulates the state of settings related to user switching. */ +data class UserSwitcherSettingsModel( + val isSimpleUserSwitcher: Boolean = false, + val isAddUsersFromLockscreen: Boolean = false, + val isUserSwitcherEnabled: Boolean = false, +) 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 035638800f9c..b85e85d01b69 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 @@ -18,9 +18,13 @@ package com.android.systemui.user.data.repository import android.content.Context +import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import com.android.internal.util.UserIcons import com.android.systemui.R @@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall 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.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Acts as source of truth for user related data. @@ -55,6 +80,18 @@ interface UserRepository { /** List of available user-related actions. */ val actions: Flow> + /** User switcher related settings. */ + val userSwitcherSettings: Flow + + /** List of all users on the device. */ + val userInfos: Flow> + + /** [UserInfo] of the currently-selected user. */ + val selectedUserInfo: Flow + + /** User ID of the last non-guest selected user. */ + val lastSelectedNonGuestUserId: Int + /** Whether actions are available even when locked. */ val isActionableWhenLocked: Flow @@ -62,7 +99,23 @@ interface UserRepository { val isGuestUserAutoCreated: Boolean /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean + var isGuestUserResetting: Boolean + + /** Whether we've scheduled the creation of a guest user. */ + val isGuestUserCreationScheduled: AtomicBoolean + + /** The user of the secondary service. */ + var secondaryUserId: Int + + /** Whether refresh users should be paused. */ + var isRefreshUsersPaused: Boolean + + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ + fun refreshUsers() + + fun getSelectedUserInfo(): UserInfo + + fun isSimpleUserSwitcher(): Boolean } @SysUISingleton @@ -71,9 +124,31 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - controller: UserSwitcherController, + private val controller: UserSwitcherController, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val globalSettings: GlobalSettings, + private val tracker: UserTracker, + private val featureFlags: FeatureFlags, ) : UserRepository { + private val isNewImpl: Boolean + get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + + private val _userSwitcherSettings = MutableStateFlow(null) + override val userSwitcherSettings: Flow = + _userSwitcherSettings.asStateFlow().filterNotNull() + + private val _userInfos = MutableStateFlow?>(null) + override val userInfos: Flow> = _userInfos.filterNotNull() + + private val _selectedUserInfo = MutableStateFlow(null) + override val selectedUserInfo: Flow = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private set + private val userRecords: Flow> = conflatedCallbackFlow { fun send() { trySendWithFailureLogging( @@ -99,11 +174,148 @@ constructor( override val actions: Flow> = userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - override val isActionableWhenLocked: Flow = controller.isAddUsersFromLockScreenEnabled + override val isActionableWhenLocked: Flow = + if (isNewImpl) { + emptyFlow() + } else { + controller.isAddUsersFromLockScreenEnabled + } + + override val isGuestUserAutoCreated: Boolean = + if (isNewImpl) { + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) + } else { + controller.isGuestUserAutoCreated + } + + private var _isGuestUserResetting: Boolean = false + override var isGuestUserResetting: Boolean = + if (isNewImpl) { + _isGuestUserResetting + } else { + controller.isGuestUserResetting + } + set(value) = + if (isNewImpl) { + _isGuestUserResetting = value + } else { + error("Not supported in the old implementation!") + } + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL - override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated + override var isRefreshUsersPaused: Boolean = false - override val isGuestUserResetting: Boolean = controller.isGuestUserResetting + init { + if (isNewImpl) { + observeSelectedUser() + observeUserSettings() + } + } + + override fun refreshUsers() { + applicationScope.launch { + val result = withContext(backgroundDispatcher) { manager.aliveUsers } + + if (result != null) { + _userInfos.value = result + } + } + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher) + } + + private fun observeSelectedUser() { + conflatedCallbackFlow { + fun send() { + trySendWithFailureLogging(tracker.userInfo, TAG) + } + + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + send() + } + } + + tracker.addCallback(callback, mainDispatcher.asExecutor()) + send() + + awaitClose { tracker.removeCallback(callback) } + } + .onEach { + if (!it.isGuest) { + lastSelectedNonGuestUserId = it.id + } + + _selectedUserInfo.value = it + } + .launchIn(applicationScope) + } + + private fun observeUserSettings() { + globalSettings + .observerFlow( + names = + arrayOf( + SETTING_SIMPLE_USER_SWITCHER, + Settings.Global.ADD_USERS_WHEN_LOCKED, + Settings.Global.USER_SWITCHER_ENABLED, + ), + userId = UserHandle.USER_SYSTEM, + ) + .onStart { emit(Unit) } // Forces an initial update. + .map { getSettings() } + .onEach { _userSwitcherSettings.value = it } + .launchIn(applicationScope) + } + + private suspend fun getSettings(): UserSwitcherSettingsModel { + return withContext(backgroundDispatcher) { + val isSimpleUserSwitcher = + globalSettings.getIntForUser( + SETTING_SIMPLE_USER_SWITCHER, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher + ) + ) { + 1 + } else { + 0 + }, + UserHandle.USER_SYSTEM, + ) != 0 + + val isAddUsersFromLockscreen = + globalSettings.getIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + val isUserSwitcherEnabled = + globalSettings.getIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + UserSwitcherSettingsModel( + isSimpleUserSwitcher = isSimpleUserSwitcher, + isAddUsersFromLockscreen = isAddUsersFromLockscreen, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } private fun UserRecord.isUser(): Boolean { return when { @@ -125,6 +337,7 @@ constructor( image = getUserImage(this), isSelected = isCurrent, isSelectable = isSwitchToEnabled || isGuest, + isGuest = isGuest, ) } @@ -162,5 +375,6 @@ constructor( companion object { private const val TAG = "UserRepository" + @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index bf7977a600e9..2e9367126a4c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -32,4 +32,7 @@ data class UserModel( val isSelected: Boolean, /** Whether this use is selectable. A non-selectable user cannot be switched to. */ val isSelectable: Boolean, + /** Whether this model represents the guest user. */ + // TODO(b/246631653): remove this default value it was only here to be able to split up CLs + val isGuest: Boolean = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt new file mode 100644 index 000000000000..0b8257da8fb5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -0,0 +1,48 @@ +/* + * 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.util.settings + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Kotlin extension functions for [SettingsProxy]. */ +object SettingsProxyExt { + + /** Returns a flow of [Unit] that is invoked each time that content is updated. */ + fun SettingsProxy.observerFlow( + vararg names: String, + @UserIdInt userId: Int = UserHandle.USER_CURRENT, + ): Flow { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + names.forEach { name -> registerContentObserverForUser(name, observer, userId) } + + awaitClose { unregisterContentObserver(observer) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt new file mode 100644 index 000000000000..4a8e0552d778 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -0,0 +1,204 @@ +/* + * 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.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { + + @Before + fun setUp() { + super.setUp(isRefactored = true) + } + + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + hasGuest = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + private fun setUpUsers( + count: Int, + hasGuest: Boolean = false, + selectedIndex: Int = 0, + ): List { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = hasGuest && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6fec343d036c..6568f5f680e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,201 +17,54 @@ package com.android.systemui.user.data.repository -import android.content.pm.UserInfo import android.os.UserManager -import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -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.capture -import com.google.common.truth.Truth.assertThat +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.Captor +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplTest : SysuiTestCase() { +abstract class UserRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var manager: UserManager - @Mock private lateinit var controller: UserSwitcherController - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var controller: UserSwitcherController - private lateinit var underTest: UserRepositoryImpl + protected lateinit var underTest: UserRepositoryImpl - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = - UserRepositoryImpl( - appContext = context, - manager = manager, - controller = controller, - ) - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + protected lateinit var globalSettings: FakeSettings + protected lateinit var tracker: FakeUserTracker + protected lateinit var featureFlags: FakeFeatureFlags - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actopms - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - job.cancel() - } + protected fun setUp(isRefactored: Boolean) { + MockitoAnnotations.initMocks(this) - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) + globalSettings = FakeSettings() + tracker = FakeUserTracker() + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored) } - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, + protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + return UserRepositoryImpl( + appContext = context, + manager = manager, + controller = controller, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + globalSettings = globalSettings, + tracker = tracker, + featureFlags = featureFlags, ) } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt new file mode 100644 index 000000000000..d4b41c18e123 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -0,0 +1,205 @@ +/* + * 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.data.repository + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.source.UserRecord +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.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Captor + private lateinit var userSwitchCallbackCaptor: + ArgumentCaptor + + @Before + fun setUp() { + super.setUp(isRefactored = false) + + whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) + whenever(controller.isGuestUserAutoCreated).thenReturn(false) + whenever(controller.isGuestUserResetting).thenReturn(false) + + underTest = create() + } + + @Test + fun `users - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `users - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `users - does not include actions`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List? = null + val job = underTest.users.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)?.id).isEqualTo(0) + assertThat(models?.get(0)?.isSelected).isTrue() + assertThat(models?.get(1)?.id).isEqualTo(1) + assertThat(models?.get(1)?.isSelected).isFalse() + assertThat(models?.get(2)?.id).isEqualTo(2) + assertThat(models?.get(2)?.isSelected).isFalse() + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createUserRecord(1), + createUserRecord(2), + ) + ) + var id: Int? = null + val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + + assertThat(id).isEqualTo(0) + + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0), + createUserRecord(1), + createUserRecord(2, isSelected = true), + ) + ) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + userSwitchCallbackCaptor.value.onUserSwitched() + assertThat(id).isEqualTo(2) + + job.cancel() + } + + @Test + fun `actions - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `actions - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `actions - does not include users`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List? = null + val job = underTest.actions.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) + assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) + assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + job.cancel() + } + + private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { + return UserRecord( + info = UserInfo(id, "name$id", 0), + isCurrent = isSelected, + ) + } + + private fun createActionRecord(action: UserActionModel): UserRecord { + return UserRecord( + isAddUser = action == UserActionModel.ADD_USER, + isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, + isGuest = action == UserActionModel.ENTER_GUEST_MODE, + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index b2b176420e40..9726bf83b263 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -26,20 +26,24 @@ import java.util.concurrent.Executor /** A fake [UserTracker] to be used in tests. */ class FakeUserTracker( - userId: Int = 0, - userHandle: UserHandle = UserHandle.of(userId), - userInfo: UserInfo = mock(), - userProfiles: List = emptyList(), + private var _userId: Int = 0, + private var _userHandle: UserHandle = UserHandle.of(_userId), + private var _userInfo: UserInfo = mock(), + private var _userProfiles: List = emptyList(), userContentResolver: ContentResolver = MockContentResolver(), userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { val callbacks = mutableListOf() - override val userId: Int = userId - override val userHandle: UserHandle = userHandle - override val userInfo: UserInfo = userInfo - override val userProfiles: List = userProfiles + override val userId: Int + get() = _userId + override val userHandle: UserHandle + get() = _userHandle + override val userInfo: UserInfo + get() = _userInfo + override val userProfiles: List + get() = _userProfiles override val userContentResolver: ContentResolver = userContentResolver override val userContext: Context = userContext @@ -55,4 +59,13 @@ class FakeUserTracker( override fun createCurrentUserContext(context: Context): Context { return onCreateCurrentUserContext(context) } + + fun set(userInfos: List, selectedUserIndex: Int) { + _userProfiles = userInfos + _userInfo = userInfos[selectedUserIndex] + _userId = _userInfo.id + _userHandle = UserHandle.of(_userId) + + callbacks.forEach { it.onUserChanged(_userId, userContext) } + } } 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 20f1e367944f..4df8aa42ea2f 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 @@ -17,12 +17,18 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { @@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository { private val _actions = MutableStateFlow>(emptyList()) override val actions: Flow> = _actions.asStateFlow() + private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) + override val userSwitcherSettings: Flow = + _userSwitcherSettings.asStateFlow() + + private val _userInfos = MutableStateFlow>(emptyList()) + override val userInfos: Flow> = _userInfos.asStateFlow() + + private val _selectedUserInfo = MutableStateFlow(null) + override val selectedUserInfo: Flow = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private val _isActionableWhenLocked = MutableStateFlow(false) override val isActionableWhenLocked: Flow = _isActionableWhenLocked.asStateFlow() private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated - private var _isGuestUserResetting: Boolean = false - override val isGuestUserResetting: Boolean - get() = _isGuestUserResetting + + override var isGuestUserResetting: Boolean = false + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL + + override var isRefreshUsersPaused: Boolean = false + + var refreshUsersCallCount: Int = 0 + private set + + override fun refreshUsers() { + refreshUsersCallCount++ + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return _userSwitcherSettings.value.isSimpleUserSwitcher + } + + fun setUserInfos(infos: List) { + _userInfos.value = infos + } + + suspend fun setSelectedUserInfo(userInfo: UserInfo) { + check(_userInfos.value.contains(userInfo)) { + "Cannot select the following user, it is not in the list of user infos: $userInfo!" + } + + _selectedUserInfo.value = userInfo + yield() + } + + suspend fun setSettings(settings: UserSwitcherSettingsModel) { + _userSwitcherSettings.value = settings + yield() + } fun setUsers(models: List) { _users.value = models } - fun setSelectedUser(userId: Int) { + suspend fun setSelectedUser(userId: Int) { check(_users.value.find { it.id == userId } != null) { "Cannot select a user with ID $userId - no user with that ID found!" } @@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository { } } ) + yield() } fun setActions(models: List) { @@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } - - fun setGuestUserResetting(value: Boolean) { - _isGuestUserResetting = value - } } -- cgit v1.2.3-59-g8ed1b From 0fd4ee12809fe40dbf07a0933c21960cea5c5e4f Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 28 Sep 2022 09:51:39 -0700 Subject: RefreshScheduler. A piece of business logic used by interactors (in next CLs) that can pause, unpause, and schedule "refresh users" logic to happen. Bug: 246631653 Test: Included in CL Change-Id: I18d51923bfaad7a06fc6cb372f5139f0fff65411 --- .../domain/interactor/RefreshUsersScheduler.kt | 75 +++++++++++++++++ .../domain/interactor/RefreshUsersSchedulerTest.kt | 95 ++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt new file mode 100644 index 000000000000..8f36821a955e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt @@ -0,0 +1,75 @@ +/* + * 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.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */ +@SysUISingleton +class RefreshUsersScheduler +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + private val repository: UserRepository, +) { + private var scheduledUnpauseJob: Job? = null + private var isPaused = false + + fun pause() { + applicationScope.launch(mainDispatcher) { + isPaused = true + scheduledUnpauseJob?.cancel() + scheduledUnpauseJob = + applicationScope.launch { + delay(PAUSE_REFRESH_USERS_TIMEOUT_MS) + unpauseAndRefresh() + } + } + } + + fun unpauseAndRefresh() { + applicationScope.launch(mainDispatcher) { + isPaused = false + refreshIfNotPaused() + } + } + + fun refreshIfNotPaused() { + applicationScope.launch(mainDispatcher) { + if (isPaused) { + return@launch + } + + repository.refreshUsers() + } + } + + companion object { + private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt new file mode 100644 index 000000000000..593ce1f0a2f5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt @@ -0,0 +1,95 @@ +/* + * 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.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class RefreshUsersSchedulerTest : SysuiTestCase() { + + private lateinit var underTest: RefreshUsersScheduler + + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + repository = FakeUserRepository() + } + + @Test + fun `pause - prevents the next refresh from happening`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.refreshIfNotPaused() + assertThat(repository.refreshUsersCallCount).isEqualTo(0) + } + + @Test + fun `unpauseAndRefresh - forces the refresh even when paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.unpauseAndRefresh() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + @Test + fun `refreshIfNotPaused - refreshes when not paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.refreshIfNotPaused() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} -- cgit v1.2.3-59-g8ed1b From 7c822664bedfdbf13b39957d3c41d6cfb41ef10c Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 28 Sep 2022 09:53:10 -0700 Subject: GuestUserInteractor. Business logic surrounding guest user. Bug: 246631653 Test: included in CL Change-Id: I7363f64f93fff803ecabd3db4050fcdc9981aaaf --- .../user/domain/interactor/GuestUserInteractor.kt | 322 ++++++++++++++++++++ .../user/domain/model/ShowDialogRequestModel.kt | 41 +++ .../domain/interactor/GuestUserInteractorTest.kt | 336 +++++++++++++++++++++ 3 files changed, 699 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt new file mode 100644 index 000000000000..27748128a557 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -0,0 +1,322 @@ +/* + * 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.domain.interactor + +import android.annotation.UserIdInt +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.WindowManagerGlobal +import android.widget.Toast +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** Encapsulates business logic to interact with guest user data and systems. */ +@SysUISingleton +class GuestUserInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val manager: UserManager, + private val repository: UserRepository, + private val deviceProvisionedController: DeviceProvisionedController, + private val devicePolicyManager: DevicePolicyManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val uiEventLogger: UiEventLogger, +) { + /** Whether the device is configured to always have a guest user available. */ + val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + + /** Whether the guest user is currently being reset. */ + val isGuestUserResetting: Boolean = repository.isGuestUserResetting + + /** Notifies that the device has finished booting. */ + fun onDeviceBootCompleted() { + applicationScope.launch { + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + return@launch + } + + suspendCancellableCoroutine { continuation -> + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onDeviceProvisionedChanged() { + continuation.resumeWith(Result.success(Unit)) + deviceProvisionedController.removeCallback(this) + } + } + + deviceProvisionedController.addCallback(callback) + } + + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + } + } + } + + /** Creates a guest user and switches to it. */ + fun createAndSwitchTo( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + selectUser: (userId: Int) -> Unit, + ) { + applicationScope.launch { + val newGuestUserId = create(showDialog, dismissDialog) + if (newGuestUserId != UserHandle.USER_NULL) { + selectUser(newGuestUserId) + } + } + } + + /** Exits the guest user, switching back to the last non-guest user or to the default user. */ + fun exit( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUserInfo = repository.getSelectedUserInfo() + if (currentUserInfo.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " (${currentUserInfo.id})" + ) + return + } + + if (!currentUserInfo.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + applicationScope.launch { + var newUserId = UserHandle.USER_SYSTEM + if (targetUserId == UserHandle.USER_NULL) { + // When a target user is not specified switch to last non guest user: + val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId + if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) { + val info = + withContext(backgroundDispatcher) { + manager.getUserInfo(lastSelectedNonGuestUserHandle) + } + if (info != null && info.isEnabled && info.supportsSwitchToByUser()) { + newUserId = info.id + } + } + } else { + newUserId = targetUserId + } + + if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE) + remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser) + } else { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH) + switchUser(newUserId) + } + } + } + + /** + * Guarantees that the guest user is present on the device, creating it if needed and if allowed + * to. + */ + suspend fun guaranteePresent() { + if (!isDeviceAllowedToAddGuest()) { + return + } + + val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() } + if (guestUser == null) { + scheduleCreation() + } + } + + /** Removes the guest user from the device. */ + private suspend fun remove( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUser: UserInfo = repository.getSelectedUserInfo() + if (currentUser.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " ($currentUser.id)" + ) + return + } + + if (!currentUser.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + val marked = + withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) } + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId") + return + } + + if (targetUserId == UserHandle.USER_NULL) { + // Create a new guest in the foreground, and then immediately switch to it + val newGuestId = create(showDialog, dismissDialog) + if (newGuestId == UserHandle.USER_NULL) { + Log.e(TAG, "Could not create new guest, switching back to system user") + switchUser(UserHandle.USER_SYSTEM) + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + try { + WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) + } catch (e: RemoteException) { + Log.e( + TAG, + "Couldn't remove guest because ActivityManager or WindowManager is dead" + ) + } + return + } + + switchUser(newGuestId) + + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + } else { + if (repository.isGuestUserAutoCreated) { + repository.isGuestUserResetting = true + } + switchUser(targetUserId) + manager.removeUser(currentUser.id) + } + } + + /** + * Creates the guest user and adds it to the device. + * + * @param showDialog A function to invoke to show a dialog. + * @param dismissDialog A function to invoke to dismiss a dialog. + * @return The user ID of the newly-created guest user. + */ + private suspend fun create( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + ): Int { + return withContext(mainDispatcher) { + showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + val guestUserId = createInBackground() + dismissDialog() + if (guestUserId != UserHandle.USER_NULL) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD) + } else { + Toast.makeText( + applicationContext, + com.android.settingslib.R.string.add_guest_failed, + Toast.LENGTH_SHORT, + ) + .show() + } + + guestUserId + } + } + + /** Schedules the creation of the guest user. */ + private suspend fun scheduleCreation() { + if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) { + return + } + + withContext(backgroundDispatcher) { + val newGuestUserId = createInBackground() + repository.isGuestUserCreationScheduled.set(false) + repository.isGuestUserResetting = false + if (newGuestUserId == UserHandle.USER_NULL) { + Log.w(TAG, "Could not create new guest while exiting existing guest") + // Refresh users so that we still display "Guest" if + // config_guestUserAutoCreated=true + refreshUsersScheduler.refreshIfNotPaused() + } + } + } + + /** + * Creates a guest user and return its multi-user user ID. + * + * This method does not check if a guest already exists before it makes a call to [UserManager] + * to create a new one. + * + * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if + * the guest couldn't be created. + */ + @UserIdInt + private suspend fun createInBackground(): Int { + return withContext(backgroundDispatcher) { + try { + val guestUser = manager.createGuest(applicationContext) + if (guestUser != null) { + guestUser.id + } else { + Log.e( + TAG, + "Couldn't create guest, most likely because there already exists one!" + ) + UserHandle.USER_NULL + } + } catch (e: UserManager.UserOperationException) { + Log.e(TAG, "Couldn't create guest user!", e) + UserHandle.USER_NULL + } + } + } + + private fun isDeviceAllowedToAddGuest(): Boolean { + return deviceProvisionedController.isDeviceProvisioned && + !devicePolicyManager.isDeviceManaged + } + + companion object { + private const val TAG = "GuestUserInteractor" + } +} 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 new file mode 100644 index 000000000000..08d7c5a26a25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -0,0 +1,41 @@ +/* + * 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.domain.model + +import android.os.UserHandle + +/** Encapsulates a request to show a dialog. */ +sealed class ShowDialogRequestModel { + data class ShowAddUserDialog( + val userHandle: UserHandle, + val isKeyguardShowing: Boolean, + val showEphemeralMessage: Boolean, + ) : ShowDialogRequestModel() + + data class ShowUserCreationDialog( + val isGuest: Boolean, + ) : ShowDialogRequestModel() + + data class ShowExitGuestDialog( + val guestUserId: Int, + val targetUserId: Int, + val isGuestEphemeral: Boolean, + val isKeyguardShowing: Boolean, + val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, + ) : ShowDialogRequestModel() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt new file mode 100644 index 000000000000..6b4c9ed38b47 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -0,0 +1,336 @@ +/* + * 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.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope +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.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class GuestUserInteractorTest : SysuiTestCase() { + + @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 showDialog: (ShowDialogRequestModel) -> Unit + @Mock private lateinit var dismissDialog: () -> Unit + @Mock private lateinit var selectUser: (Int) -> Unit + @Mock private lateinit var switchUser: (Int) -> Unit + + private lateinit var underTest: GuestUserInteractor + + private lateinit var scope: TestCoroutineScope + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO) + + scope = TestCoroutineScope() + repository = FakeUserRepository() + repository.setUserInfos(ALL_USERS) + + underTest = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = repository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = repository, + ), + uiEventLogger = uiEventLogger, + ) + } + + @Test + fun `onDeviceBootCompleted - allowed to add - create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd() + + underTest.onDeviceBootCompleted() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController, never()).addCallback(any()) + } + + @Test + fun `onDeviceBootCompleted - await provisioning - and create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd(isAllowed = false) + underTest.onDeviceBootCompleted() + val captor = + kotlinArgumentCaptor() + verify(deviceProvisionedController).addCallback(captor.capture()) + + setAllowedToAdd(isAllowed = true) + captor.value.onDeviceProvisionedChanged() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController).removeCallback(captor.value) + } + + @Test + fun createAndSwitchTo() = + runBlocking(IMMEDIATE) { + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser).invoke(GUEST_USER_INFO.id) + } + + @Test + fun `createAndSwitchTo - fails to create - does not switch to`() = + runBlocking(IMMEDIATE) { + whenever(manager.createGuest(any())).thenReturn(null) + + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser, never()).invoke(anyInt()) + } + + @Test + fun `exit - returns to target user`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - returns to last non-guest`() = + runBlocking(IMMEDIATE) { + val expectedUserId = NON_GUEST_USER_INFO.id + whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO) + repository.lastSelectedNonGuestUserId = expectedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(expectedUserId) + } + + @Test + fun `exit - last non-guest was removed - returns to system`() = + runBlocking(IMMEDIATE) { + val removedUserId = 310 + repository.lastSelectedNonGuestUserId = removedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(UserHandle.USER_SYSTEM) + } + + @Test + fun `exit - guest was ephemeral - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO)) + repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id) + verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - force remove guest - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = true, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `exit - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + private fun setAllowedToAdd(isAllowed: Boolean = true) { + whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed) + whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed) + } + + private fun verifyDidNotExit() { + verify(manager, never()).getUserInfo(anyInt()) + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(uiEventLogger, never()).log(any()) + verify(showDialog, never()).invoke(any()) + verify(dismissDialog, never()).invoke() + verify(switchUser, never()).invoke(anyInt()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val NON_GUEST_USER_INFO = + UserInfo( + /* id= */ 818, + /* name= */ "non_guest", + /* flags= */ 0, + ) + private val GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val EPHEMERAL_GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_EPHEMERAL, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val ALL_USERS = + listOf( + NON_GUEST_USER_INFO, + GUEST_USER_INFO, + ) + } +} -- cgit v1.2.3-59-g8ed1b From fbbb1a8bd57ccc49200f1ddb8e31a0fc55e2fd46 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 28 Sep 2022 10:04:24 -0700 Subject: UserInteractor changes to not depend on USC Makes the UserInteractor no longer depend on UserSwitcherController, based on a flag. Bug: 246631653 Test: tests included, also manually tested with followup CL(s) Change-Id: Ia8783d461c5366dc437fad3308b6a776704ffad3 --- .../user/domain/interactor/UserActionsUtil.kt | 114 +++++ .../user/domain/interactor/UserInteractor.kt | 565 +++++++++++++++++++-- .../interactor/UserInteractorRefactoredTest.kt | 554 ++++++++++++++++++++ .../user/domain/interactor/UserInteractorTest.kt | 219 +++----- .../interactor/UserInteractorUnrefactoredTest.kt | 188 +++++++ .../user/ui/viewmodel/UserSwitcherViewModelTest.kt | 53 +- .../systemui/broadcast/FakeBroadcastDispatcher.kt | 16 +- 7 files changed, 1502 insertions(+), 207 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt new file mode 100644 index 000000000000..1b4746a99f8f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt @@ -0,0 +1,114 @@ +/* + * 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.domain.interactor + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.user.data.repository.UserRepository + +/** Utilities related to user management actions. */ +object UserActionsUtil { + + /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */ + fun canCreateGuest( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + return currentUserCanCreateUsers(manager, repository) || + anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + } + + /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */ + fun canCreateUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + if ( + !currentUserCanCreateUsers(manager, repository) && + !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + ) { + return false + } + + return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) + } + + /** + * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise. + */ + fun canCreateSupervisedUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + supervisedUserPackageName: String? + ): Boolean { + if (supervisedUserPackageName.isNullOrEmpty()) { + return false + } + + return canCreateUser( + manager, + repository, + isUserSwitcherEnabled, + isAddUsersFromLockScreenEnabled + ) + } + + /** + * Returns `true` if the current user is allowed to add users to the device; `false` otherwise. + */ + private fun currentUserCanCreateUsers( + manager: UserManager, + repository: UserRepository, + ): Boolean { + val currentUser = repository.getSelectedUserInfo() + if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) { + return false + } + + return systemCanCreateUsers(manager) + } + + /** Returns `true` if the system can add users to the device; `false` otherwise. */ + private fun systemCanCreateUsers( + manager: UserManager, + ): Boolean { + return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM) + } + + /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */ + private fun anyoneCanCreateUsers( + manager: UserManager, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled + } +} 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 3c5b9697c013..b923850e4cbf 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 @@ -17,94 +17,573 @@ package com.android.systemui.user.domain.interactor +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.app.ActivityManager +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager import android.provider.Settings +import android.util.Log +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.SystemUISecondaryUserService +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.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +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.kotlin.pairwise +import java.util.Collections +import java.util.WeakHashMap import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @SysUISingleton class UserInteractor @Inject constructor( - repository: UserRepository, + @Application private val applicationContext: Context, + private val repository: UserRepository, private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, - keyguardInteractor: KeyguardInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, ) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + fun interface UserCallback { + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val isNewImpl: Boolean + get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbacks = Collections.newSetFromMap(WeakHashMap()) + /** List of current on-device users to select from. */ - val users: Flow> = repository.users + val users: Flow> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + } else { + repository.users + } /** The currently-selected user. */ - val selectedUser: Flow = repository.selectedUser + val selectedUser: Flow + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + ) + } + } else { + repository.selectedUser + } /** List of user-switcher related actions that are available. */ - val actions: Flow> = - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because - // that's a user - // switcher specific action that is not known to the our data source - // or other - // features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } + val actions: Flow> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } + + if (isDeviceLocked && !settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. + return@buildList + } + + if ( + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.ADD_USER) + } + + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) + } } - } else { - // If not actionable it means that we're not allowed to show actions when locked - // and we - // are locked. Therefore, we should show no actions. - flowOf(emptyList()) } + } else { + combine( + repository.isActionableWhenLocked, + keyguardInteractor.isKeyguardShowing, + ) { isActionableWhenLocked, isLocked -> + isActionableWhenLocked || !isLocked + } + .flatMapLatest { isActionable -> + if (isActionable) { + repository.actions.map { actions -> + actions + + if (actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT + // because that's a user switcher specific action that is + // not known to the our data source or other features. + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + // If no actions, don't add the navigate action. + emptyList() + } + } + } else { + // If not actionable it means that we're not allowed to show actions + // when + // locked and we are locked. Therefore, we should show no actions. + flowOf(emptyList()) + } + } } /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = repository.isGuestUserResetting + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + private val _dialogShowRequests = MutableStateFlow(null) + val dialogShowRequests: Flow = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow(null) + val dialogDismissRequests: Flow = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = + if (isNewImpl) { + repository.isSimpleUserSwitcher() + } else { + error("Not supported in the old implementation!") + } + + fun addCallback(callback: UserCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: UserCallback) { + callbacks.remove(callback) + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + init { + if (isNewImpl) { + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + } + } + + fun onDeviceBootCompleted() { + guestUserInteractor.onDeviceBootCompleted() + } /** Switches to the user with the given user ID. */ fun selectUser( - userId: Int, + newlySelectedUserId: Int, ) { - controller.onUserSelected(userId, /* dialogShower= */ null) + if (isNewImpl) { + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && + currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + switchUser(newlySelectedUserId) + } else { + controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null) + } } /** Executes the given action. */ fun executeAction(action: UserActionModel) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + if (isNewImpl) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + this::selectUser, + ) + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + ) + ) + } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ false, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } else { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) + UserActionModel.ADD_USER -> controller.showAddUserDialog(null) + UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } + } + + private fun exitGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + ) { + guestUserInteractor.exit( + guestUserId = guestUserId, + targetUserId = targetUserId, + forceRemoveGuestOnExit = forceRemoveGuestOnExit, + showDialog = this::showDialog, + dismissDialog = this::dismissDialog, + switchUser = this::switchUser, + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and lantecy like in the old impl. + refreshUsersScheduler.pause() + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + private suspend fun onBroadcastReceived( + intent: Intent, + previousUserInfo: UserInfo?, + ) { + val shouldRefreshAllUsers = + when (intent.action) { + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + callbacks.forEach { it.onUserStateChanged() } + restartSecondaryService(selectedUserId) + } + if (guestUserInteractor.isGuestUserAutoCreated) { + guestUserInteractor.guaranteePresent() + } + true + } + Intent.ACTION_USER_INFO_CHANGED -> true + Intent.ACTION_USER_UNLOCKED -> { + // If we unlocked the system user, we should refresh all users. + intent.getIntExtra( + Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL, + ) == UserHandle.USER_SYSTEM + } + else -> true + } + + if (shouldRefreshAllUsers) { + refreshUsersScheduler.unpauseAndRefresh() + } + } + + private fun restartSecondaryService(@UserIdInt userId: Int) { + val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java) + // Disconnect from the old secondary user's service + val secondaryUserId = repository.secondaryUserId + if (secondaryUserId != UserHandle.USER_NULL) { + applicationContext.stopServiceAsUser( + intent, + UserHandle.of(secondaryUserId), + ) + repository.secondaryUserId = UserHandle.USER_NULL + } + + // Connect to the new secondary user's service (purely to ensure that a persistent + // SystemUI application is created for that user) + if (userId != UserHandle.USER_SYSTEM) { + applicationContext.startServiceAsUser( + intent, + UserHandle.of(userId), + ) + repository.secondaryUserId = userId + } + } + + private suspend fun toUserModels( + userInfos: List, + selectedUserId: Int, + isUserSwitcherEnabled: Boolean, + ): List { + val canSwitchUsers = canSwitchUsers(selectedUserId) + + return userInfos + // The guest user should go in the last position. + .sortedBy { it.isGuest } + .mapNotNull { userInfo -> + toUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } + + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + val userId = userInfo.id + val isSelected = userId == selectedUserId + + return when { + // When the user switcher is not enabled in settings, we only show the primary user. + !isUserSwitcherEnabled && !userInfo.isPrimary -> null + + // We avoid showing disabled users. + !userInfo.isEnabled -> null + userInfo.isGuest -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, + ) + userInfo.supportsSwitchToByUser() -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, ) + else -> null } } + + private suspend fun canSwitchUsers(selectedUserId: Int): Boolean { + return withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle)) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested. + val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) } + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + companion object { + private const val TAG = "UserInteractor" + } } 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 new file mode 100644 index 000000000000..68d710560ebf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -0,0 +1,554 @@ +/* + * 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.domain.interactor + +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +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.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.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +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.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorRefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return true + } + + @Before + override fun setUp() { + super.setUp() + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE)) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.executeAction(UserActionModel.ADD_USER) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = guestUserInfo.id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + private fun assertUsers( + models: List?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON: Bitmap = mock() + private val GUEST_ICON: Drawable = mock() + private 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 e914e2e0a1da..cdfe9208e0b8 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 @@ -17,51 +17,60 @@ package com.android.systemui.user.domain.interactor -import androidx.test.filters.SmallTest +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.os.UserManager +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +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 +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { +abstract class UserInteractorTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController - @Mock private lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var controller: UserSwitcherController + @Mock protected lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var activityManager: ActivityManager + @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock protected lateinit var devicePolicyManager: DevicePolicyManager + @Mock protected lateinit var uiEventLogger: UiEventLogger - private lateinit var underTest: UserInteractor + protected lateinit var underTest: UserInteractor - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var userRepository: FakeUserRepository + protected lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var telephonyRepository: FakeTelephonyRepository - @Before - fun setUp() { + abstract fun isRefactored(): Boolean + + open fun setUp() { MockitoAnnotations.initMocks(this) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() + telephonyRepository = FakeTelephonyRepository() + val applicationScope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = applicationScope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, @@ -69,142 +78,34 @@ class UserInteractorTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), - ) - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored()) + }, + manager = manager, + applicationScope = applicationScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = applicationScope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt new file mode 100644 index 000000000000..c3a9705bf6ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -0,0 +1,188 @@ +/* + * 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.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +open class UserInteractorUnrefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return false + } + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun `actions - not actionable when locked and locked - no actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(true) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions).isEmpty() + job.cancel() + } + + @Test + fun `actions - not actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(false) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(false) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(true) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun selectUser() { + val userId = 3 + + underTest.selectUser(userId) + + verify(controller).onUserSelected(eq(userId), nullable()) + } + + @Test + fun `executeAction - guest`() { + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + verify(controller).createAndSwitchToGuestUser(nullable()) + } + + @Test + fun `executeAction - add user`() { + underTest.executeAction(UserActionModel.ADD_USER) + + verify(controller).showAddUserDialog(nullable()) + } + + @Test + fun `executeAction - add supervised user`() { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(controller).startSupervisedUserActivity() + } + + @Test + fun `executeAction - manage users`() { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} 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 ef4500df3600..3a84064be61d 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 @@ -17,17 +17,28 @@ package com.android.systemui.user.ui.viewmodel +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase 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 import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor 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.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var controller: UserSwitcherController @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 private lateinit var underTest: UserSwitcherViewModel @@ -66,17 +83,51 @@ class UserSwitcherViewModelTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, false) + val scope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserSwitcherViewModel.Factory( userInteractor = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, - ) + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = scope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ), ), powerInteractor = PowerInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt index 53dcc8d269c9..bb646f09b774 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt @@ -37,10 +37,18 @@ class FakeBroadcastDispatcher( dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker -) : BroadcastDispatcher( - context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) { +) : + BroadcastDispatcher( + context, + looper, + executor, + dumpManager, + logger, + userTracker, + PendingRemovalStore(logger) + ) { - private val registeredReceivers = ArraySet() + val registeredReceivers = ArraySet() override fun registerReceiverWithHandler( receiver: BroadcastReceiver, @@ -78,4 +86,4 @@ class FakeBroadcastDispatcher( } registeredReceivers.clear() } -} \ No newline at end of file +} -- cgit v1.2.3-59-g8ed1b From ae0fcebd92358c38730c64b02c736a3c9a2d787f Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 27 Sep 2022 18:04:04 -0700 Subject: user UI layer changes to support USC dep removal Minor changes to the view-model class and the addition of a UserSwitcherDialogCoordinator to be able to show and dismiss dialogs based on state exposed by the interactor. Bug: 246631653 Test: unit tests for new coordinator class included, manually verified the operations of the full-screen user switcher, the smaller dialog, the footer in quick settings, and the dropdown switcher on the lock-screen bouncer. Change-Id: I058541bb5c31023735121bbad54b93afba73a89a --- .../src/com/android/systemui/user/UserModule.java | 2 + .../systemui/user/shared/model/UserModel.kt | 3 +- .../systemui/user/ui/dialog/UserDialogModule.kt | 33 ++++++ .../ui/dialog/UserSwitcherDialogCoordinator.kt | 122 +++++++++++++++++++++ .../user/ui/viewmodel/UserSwitcherViewModel.kt | 56 ++++++++-- .../user/ui/viewmodel/UserSwitcherViewModelTest.kt | 36 +++--- 6 files changed, 227 insertions(+), 25 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 5b522dcc4885..0c72b78a3c46 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -20,6 +20,7 @@ import android.app.Activity; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; +import com.android.systemui.user.ui.dialog.UserDialogModule; import dagger.Binds; import dagger.Module; @@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap; */ @Module( includes = { + UserDialogModule.class, UserRepositoryModule.class, } ) diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index 2e9367126a4c..2095683ccb4c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -33,6 +33,5 @@ data class UserModel( /** Whether this use is selectable. A non-selectable user cannot be switched to. */ val isSelectable: Boolean, /** Whether this model represents the guest user. */ - // TODO(b/246631653): remove this default value it was only here to be able to split up CLs - val isGuest: Boolean = false, + val isGuest: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt new file mode 100644 index 000000000000..c1d2f4788147 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt @@ -0,0 +1,33 @@ +/* + * 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.dialog + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface UserDialogModule { + + @Binds + @IntoMap + @ClassKey(UserSwitcherDialogCoordinator::class) + fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable +} 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 new file mode 100644 index 000000000000..690e987bd9ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -0,0 +1,122 @@ +/* + * 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.dialog + +import android.app.Dialog +import android.content.Context +import com.android.settingslib.users.UserCreatingDialog +import com.android.systemui.CoreStartable +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.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Coordinates dialogs for user switcher logic. */ +@SysUISingleton +class UserSwitcherDialogCoordinator +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val interactor: UserInteractor, + private val featureFlags: FeatureFlags, +) : CoreStartable(context) { + + private var currentDialog: Dialog? = null + + override fun start() { + if (!featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)) { + return + } + + startHandlingDialogShowRequests() + startHandlingDialogDismissRequests() + } + + private fun startHandlingDialogShowRequests() { + applicationScope.launch { + interactor.dialogShowRequests.filterNotNull().collect { request -> + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + currentDialog = + when (request) { + is ShowDialogRequestModel.ShowAddUserDialog -> + AddUserDialog( + context = context, + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager, + broadcastSender = broadcastSender, + dialogLaunchAnimator = dialogLaunchAnimator, + ) + is ShowDialogRequestModel.ShowUserCreationDialog -> + UserCreatingDialog( + context, + request.isGuest, + ) + is ShowDialogRequestModel.ShowExitGuestDialog -> + ExitGuestDialog( + context = context, + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager, + dialogLaunchAnimator = dialogLaunchAnimator, + onExitGuestUserListener = request.onExitGuestUser, + ) + } + + currentDialog?.show() + interactor.onDialogShown() + } + } + } + + private fun startHandlingDialogDismissRequests() { + applicationScope.launch { + interactor.dialogDismissRequests.filterNotNull().collect { + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + interactor.onDialogDismissed() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 398341d256d2..dbb8200d57d8 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.systemui.R import com.android.systemui.common.ui.drawable.CircularDrawable +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map class UserSwitcherViewModel private constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModel() { + private val isNewImpl: Boolean + get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + /** On-device users. */ val users: Flow> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -47,9 +55,6 @@ private constructor( val maximumUserColumns: Flow = users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } - /** Whether the button to open the user action menu is visible. */ - val isOpenMenuButtonVisible: Flow = userInteractor.actions.map { it.isNotEmpty() } - private val _isMenuVisible = MutableStateFlow(false) /** * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the @@ -58,9 +63,23 @@ private constructor( val isMenuVisible: Flow = _isMenuVisible /** The user action menu. */ val menu: Flow> = - userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } + userInteractor.actions.map { actions -> + if (isNewImpl && actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user + // switcher specific action that is not known to the our data source or other + // features. + actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + actions + } + .map { action -> toViewModel(action) } + } + + /** Whether the button to open the user action menu is visible. */ + val isOpenMenuButtonVisible: Flow = menu.map { it.isNotEmpty() } private val hasCancelButtonBeenClicked = MutableStateFlow(false) + private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) /** * Whether the observer should finish the experience. Once consumed, [onFinished] must be called @@ -81,6 +100,7 @@ private constructor( */ fun onFinished() { hasCancelButtonBeenClicked.value = false + isFinishRequiredDueToExecutedAction.value = false } /** Notifies that the user has clicked the "open menu" button. */ @@ -120,8 +140,10 @@ private constructor( }, // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked + // If an executed action told us to finish, we should finish, + isFinishRequiredDueToExecutedAction, + ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> + selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish } } @@ -164,13 +186,25 @@ private constructor( } else { LegacyUserUiHelper.getUserSwitcherActionTextResourceId( isGuest = model == UserActionModel.ENTER_GUEST_MODE, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, + isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, + isGuestUserResetting = guestUserInteractor.isGuestUserResetting, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddUser = model == UserActionModel.ADD_USER, ) }, - onClicked = { userInteractor.executeAction(action = model) }, + onClicked = { + userInteractor.executeAction(action = model) + // We don't finish because we want to show a dialog over the full-screen UI and + // that dialog can be dismissed in case the user changes their mind and decides not + // to add a user. + // + // We finish for all other actions because they navigate us away from the + // full-screen experience or are destructive (like changing to the guest user). + val shouldFinish = model != UserActionModel.ADD_USER + if (shouldFinish) { + isFinishRequiredDueToExecutedAction.value = true + } + }, ) } @@ -186,13 +220,17 @@ private constructor( @Inject constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") return UserSwitcherViewModel( userInteractor = userInteractor, + guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, + featureFlags = featureFlags, ) as T } 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 3a84064be61d..7fd29af2a80a 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 @@ -92,6 +92,20 @@ class UserSwitcherViewModelTest : SysuiTestCase() { mainDispatcher = IMMEDIATE, repository = userRepository, ) + val guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ) + underTest = UserSwitcherViewModel.Factory( userInteractor = @@ -115,24 +129,14 @@ class UserSwitcherViewModelTest : SysuiTestCase() { backgroundDispatcher = IMMEDIATE, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, - guestUserInteractor = - GuestUserInteractor( - applicationContext = context, - applicationScope = scope, - mainDispatcher = IMMEDIATE, - backgroundDispatcher = IMMEDIATE, - manager = manager, - repository = userRepository, - deviceProvisionedController = deviceProvisionedController, - devicePolicyManager = devicePolicyManager, - refreshUsersScheduler = refreshUsersScheduler, - uiEventLogger = uiEventLogger, - ), + guestUserInteractor = guestUserInteractor, ), powerInteractor = PowerInteractor( repository = powerRepository, ), + featureFlags = featureFlags, + guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @@ -148,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = true, isSelectable = true, + isGuest = false, ), UserModel( id = 1, @@ -155,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = true, + isGuest = false, ), UserModel( id = 2, @@ -162,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = false, + isGuest = false, ), ) ) @@ -311,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { job.cancel() } - private fun setUsers(count: Int) { + private suspend fun setUsers(count: Int) { userRepository.setUsers( (0 until count).map { index -> UserModel( @@ -320,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = index == 0, isSelectable = true, + isGuest = false, ) } ) -- cgit v1.2.3-59-g8ed1b From ea92f14c6a4095870f09baf8e28a3ce013182acd Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 29 Sep 2022 17:40:27 -0700 Subject: 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 ++++++++++++++ .../user/legacyhelper/data/LegacyUserDataHelper.kt | 135 +++++++++++++++++++++ .../interactor/UserInteractorRefactoredTest.kt | 108 ++++++++++++++++- .../user/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 9cfe7a7e48ce..46d2f3ac9ce4 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 b923850e4cbf..7d2d1224fc37 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 000000000000..8f6662f7b192 --- /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 68d710560ebf..3d5695a09ebc 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 cdfe9208e0b8..d57f84b5704a 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, -- cgit v1.2.3-59-g8ed1b From 2caeebc5df64bb1e1bf235d9ed688f8d98e0408a Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 28 Sep 2022 16:52:55 -0700 Subject: Splits up the flag. We need two flags: 1. Whether the interactor and repository depend on the controller (which is what is being checked by the logic in the CLs in this chain). Currently true, can be set to false once we finish removing the controller dependency from the interactor and repository. 2. Whether the controller depends on the interactor (currently false, cannot be turned on until after the previous flag is eliminated). This way, we can roll out the logic that severs the dependency on the controller in repo and interactors without touching legacy code which still wants to depend on the old controller (extensions of the BaseUserSwitcherAdapter class do this - a next step would be to replace those with adapters that can deal with the new data schema). Bug: 246631653 Test: manually verified the operations of the full-screen user switcher, the smaller dialog, the footer in quick settings, and the dropdown switcher on the lock-screen bouncer. Change-Id: I8b7ba0aa043aab39266e1e8fbde69697ddf235d0 --- .../src/com/android/systemui/flags/Flags.java | 23 ++++++++-- .../statusbar/policy/UserSwitcherControllerImpl.kt | 53 +++++++++++----------- .../user/data/repository/UserRepository.kt | 2 +- .../user/domain/interactor/UserInteractor.kt | 2 +- .../ui/dialog/UserSwitcherDialogCoordinator.kt | 2 +- .../user/ui/viewmodel/UserSwitcherViewModel.kt | 2 +- .../user/data/repository/UserRepositoryImplTest.kt | 2 +- .../user/domain/interactor/UserInteractorTest.kt | 2 +- .../user/ui/viewmodel/UserSwitcherViewModelTest.kt | 2 +- 9 files changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 48f5f9eda909..ad2549b9d567 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -104,9 +104,26 @@ public class Flags { public static final UnreleasedFlag MODERN_USER_SWITCHER_ACTIVITY = new UnreleasedFlag(209, true); - /** Whether the new implementation of UserSwitcherController should be used. */ - public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER = - new UnreleasedFlag(210, false); + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + *

If this is {@code false}, the interactor and repo skip the controller and directly access + * the framework APIs. + */ + public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = + new UnreleasedFlag(210, true); + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + *

When this is {@code true}, the controller does not directly access framework APIs. + * Instead, it goes through the interactor. + * + *

Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is + * {@code true} as it would created a cycle between controller -> interactor -> controller. + */ + public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = + new UnreleasedFlag(211, false); /***************************************/ // 300 - power menu diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt index e283413d566c..4932a6544d05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt @@ -37,8 +37,9 @@ constructor( @Suppress("DEPRECATION") private val oldImpl: Lazy, ) : UserSwitcherController { - private val isNewImpl: Boolean - get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + private val useInteractor: Boolean = + flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && + !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val _oldImpl: UserSwitcherControllerOldImpl get() = oldImpl.get() @@ -48,7 +49,7 @@ constructor( override val users: ArrayList get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.users @@ -56,14 +57,14 @@ constructor( override val isSimpleUserSwitcher: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isSimpleUserSwitcher } override fun init(view: View) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.init(view) @@ -72,7 +73,7 @@ constructor( override val currentUserRecord: UserRecord? get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.currentUserRecord @@ -80,7 +81,7 @@ constructor( override val currentUserName: String? get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.currentUserName @@ -90,7 +91,7 @@ constructor( userId: Int, dialogShower: UserSwitchDialogController.DialogShower? ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onUserSelected(userId, dialogShower) @@ -99,7 +100,7 @@ constructor( override val isAddUsersFromLockScreenEnabled: Flow get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isAddUsersFromLockScreenEnabled @@ -107,7 +108,7 @@ constructor( override val isGuestUserAutoCreated: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isGuestUserAutoCreated @@ -115,7 +116,7 @@ constructor( override val isGuestUserResetting: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isGuestUserResetting @@ -124,7 +125,7 @@ constructor( override fun createAndSwitchToGuestUser( dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.createAndSwitchToGuestUser(dialogShower) @@ -132,7 +133,7 @@ constructor( } override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.showAddUserDialog(dialogShower) @@ -140,7 +141,7 @@ constructor( } override fun startSupervisedUserActivity() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.startSupervisedUserActivity() @@ -148,7 +149,7 @@ constructor( } override fun onDensityOrFontScaleChanged() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onDensityOrFontScaleChanged() @@ -156,7 +157,7 @@ constructor( } override fun addAdapter(adapter: WeakReference) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.addAdapter(adapter) @@ -167,7 +168,7 @@ constructor( record: UserRecord, dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onUserListItemClicked(record, dialogShower) @@ -175,7 +176,7 @@ constructor( } override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.removeGuestUser(guestUserId, targetUserId) @@ -187,7 +188,7 @@ constructor( targetUserId: Int, forceRemoveGuestOnExit: Boolean ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) @@ -195,7 +196,7 @@ constructor( } override fun schedulePostBootGuestCreation() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.schedulePostBootGuestCreation() @@ -204,14 +205,14 @@ constructor( override val isKeyguardShowing: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isKeyguardShowing } override fun startActivity(intent: Intent) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.startActivity(intent) @@ -219,7 +220,7 @@ constructor( } override fun refreshUsers(forcePictureLoadForId: Int) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.refreshUsers(forcePictureLoadForId) @@ -227,7 +228,7 @@ constructor( } override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.addUserSwitchCallback(callback) @@ -235,7 +236,7 @@ constructor( } override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.removeUserSwitchCallback(callback) @@ -243,7 +244,7 @@ constructor( } override fun dump(pw: PrintWriter, args: Array) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.dump(pw, args) 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 b85e85d01b69..3014f39c17f8 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 @@ -134,7 +134,7 @@ constructor( ) : UserRepository { private val isNewImpl: Boolean - get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val _userSwitcherSettings = MutableStateFlow(null) override val userSwitcherSettings: Flow = 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 7d2d1224fc37..e6bb9bcbc264 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 @@ -102,7 +102,7 @@ constructor( } private val isNewImpl: Boolean - get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val supervisedUserPackageName: String? get() = 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 690e987bd9ab..6e7b5232d818 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 @@ -53,7 +53,7 @@ constructor( private var currentDialog: Dialog? = null override fun start() { - if (!featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)) { + if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { return } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index dbb8200d57d8..5b83df7b4a36 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -45,7 +45,7 @@ private constructor( ) : ViewModel() { private val isNewImpl: Boolean - get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) /** On-device users. */ val users: Flow> = diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6568f5f680e7..dcea83a55a74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -47,7 +47,7 @@ abstract class UserRepositoryImplTest : SysuiTestCase() { globalSettings = FakeSettings() tracker = FakeUserTracker() featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored) + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { 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 d57f84b5704a..8465f4f46d62 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 @@ -81,7 +81,7 @@ abstract class UserInteractorTest : SysuiTestCase() { ), featureFlags = FakeFeatureFlags().apply { - set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored()) + set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) }, manager = manager, applicationScope = testCoroutineScope, 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 7fd29af2a80a..0344e3f991e2 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 @@ -84,7 +84,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() val featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, false) + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) val scope = TestCoroutineScope() val refreshUsersScheduler = RefreshUsersScheduler( -- cgit v1.2.3-59-g8ed1b