diff options
12 files changed, 279 insertions, 70 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java index 44782529e5c3..33a08035a7b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java @@ -86,6 +86,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.wakelock.WakeLockFake; @@ -160,6 +161,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { @Mock protected DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor; @Mock + protected UserLogoutInteractor mUserLogoutInteractor; + @Mock protected ScreenLifecycle mScreenLifecycle; @Mock protected AuthController mAuthController; @@ -248,6 +251,9 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { when(mFaceHelpMessageDeferralFactory.create()).thenReturn(mFaceHelpMessageDeferral); when(mDeviceEntryFingerprintAuthInteractor.isEngaged()).thenReturn(mock(StateFlow.class)); + StateFlow mockLogoutEnabledFlow = mock(StateFlow.class); + when(mockLogoutEnabledFlow.getValue()).thenReturn(false); + when(mUserLogoutInteractor.isLogoutEnabled()).thenReturn(mockLogoutEnabledFlow); mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor); @@ -291,7 +297,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { KeyguardInteractorFactory.create(mFlags).getKeyguardInteractor(), mBiometricMessageInteractor, mDeviceEntryFingerprintAuthInteractor, - mDeviceEntryFaceAuthInteractor + mDeviceEntryFaceAuthInteractor, + mUserLogoutInteractor ); mController.init(); mController.setIndicationArea(mIndicationArea); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index b03c679a9c23..6b371d74eacc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.user.data.repository +import android.app.admin.devicePolicyManager import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager @@ -24,6 +25,7 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -57,6 +59,8 @@ class UserRepositoryImplTest : SysuiTestCase() { private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val globalSettings = kosmos.fakeGlobalSettings + private val broadcastDispatcher = kosmos.broadcastDispatcher + private val devicePolicyManager = kosmos.devicePolicyManager @Mock private lateinit var manager: UserManager @@ -317,6 +321,8 @@ class UserRepositoryImplTest : SysuiTestCase() { backgroundDispatcher = testDispatcher, globalSettings = globalSettings, tracker = tracker, + broadcastDispatcher = broadcastDispatcher, + devicePolicyManager = devicePolicyManager, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt new file mode 100644 index 000000000000..26439df45ba3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserLogoutInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val userRepository = kosmos.fakeUserRepository + private val testScope = kosmos.testScope + + private val underTest = kosmos.userLogoutInteractor + + @Before + fun setUp() { + userRepository.setUserInfos(USER_INFOS) + runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[1]) } + } + + @Test + fun logOut_doesNothing_whenAdminDisabledSecondaryLogout() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + userRepository.setSecondaryUserLogoutEnabled(false) + assertThat(isLogoutEnabled).isFalse() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount) + } + } + + @Test + fun logOut_logsOut_whenAdminEnabledSecondaryLogout() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + userRepository.setSecondaryUserLogoutEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1) + } + } + + companion object { + private val USER_INFOS = + listOf(UserInfo(0, "System user", 0), UserInfo(10, "Regular user", 0)) + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index eda07cfe8d91..b40e07019694 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -219,7 +219,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int MSG_USER_UNLOCKED = 334; private static final int MSG_ASSISTANT_STACK_CHANGED = 335; private static final int MSG_BIOMETRIC_AUTHENTICATION_CONTINUE = 336; - private static final int MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED = 337; private static final int MSG_TELEPHONY_CAPABLE = 338; private static final int MSG_TIMEZONE_UPDATE = 339; private static final int MSG_USER_STOPPED = 340; @@ -402,7 +401,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; private boolean mFingerprintDetectRunning; private boolean mIsDreaming; - private boolean mLogoutEnabled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private final FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; @@ -1739,9 +1737,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState)); } else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) { mHandler.sendEmptyMessage(MSG_SIM_SUBSCRIPTION_INFO_CHANGED); - } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals( - action)) { - mHandler.sendEmptyMessage(MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED); } } }; @@ -2328,9 +2323,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab case MSG_BIOMETRIC_AUTHENTICATION_CONTINUE: updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); break; - case MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED: - updateLogoutEnabled(); - break; case MSG_TELEPHONY_CAPABLE: updateTelephonyCapable((boolean) msg.obj); break; @@ -2496,7 +2488,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean isUserUnlocked = mUserManager.isUserUnlocked(user); mLogger.logUserUnlockedInitialState(user, isUserUnlocked); mUserIsUnlocked.put(user, isUserUnlocked); - mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); updateSecondaryLockscreenRequirement(user); List<UserInfo> allUsers = mUserManager.getUsers(); for (UserInfo userInfo : allUsers) { @@ -4062,28 +4053,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return null; // not found } - /** - * @return a cached version of DevicePolicyManager.isLogoutEnabled() - */ - public boolean isLogoutEnabled() { - return mLogoutEnabled; - } - - private void updateLogoutEnabled() { - Assert.isMainThread(); - boolean logoutEnabled = mDevicePolicyManager.isLogoutEnabled(); - if (mLogoutEnabled != logoutEnabled) { - mLogoutEnabled = logoutEnabled; - - for (int i = 0; i < mCallbacks.size(); i++) { - KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); - if (cb != null) { - cb.onLogoutEnabledChanged(); - } - } - } - } - protected int getBiometricLockoutDelay() { return BIOMETRIC_LOCKOUT_RESET_DELAY_MS; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 7ac5ac229793..fdee21bcc479 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -286,11 +286,6 @@ public class KeyguardUpdateMonitorCallback { public void onTrustAgentErrorMessage(CharSequence message) { } /** - * Called when a value of logout enabled is change. - */ - public void onLogoutEnabledChanged() { } - - /** * Called when authenticated fingerprint biometrics are cleared. */ public void onFingerprintsCleared() { } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 162047bb3b79..91b44e7a6202 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -36,7 +36,6 @@ import android.app.Dialog; import android.app.IActivityManager; import android.app.StatusBarManager; import android.app.WallpaperManager; -import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -138,6 +137,7 @@ import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.EmergencyDialerConstants; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.settings.GlobalSettings; @@ -197,7 +197,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final Context mContext; private final GlobalActionsManager mWindowManagerFuncs; private final AudioManager mAudioManager; - private final DevicePolicyManager mDevicePolicyManager; private final LockPatternUtils mLockPatternUtils; private final SelectedUserInteractor mSelectedUserInteractor; private final TelephonyListenerManager mTelephonyListenerManager; @@ -260,6 +259,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final ShadeController mShadeController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DialogTransitionAnimator mDialogTransitionAnimator; + private final UserLogoutInteractor mLogoutInteractor; private final GlobalActionsInteractor mInteractor; @VisibleForTesting @@ -344,7 +344,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Context context, GlobalActionsManager windowManagerFuncs, AudioManager audioManager, - DevicePolicyManager devicePolicyManager, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, TelephonyListenerManager telephonyListenerManager, @@ -376,11 +375,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene KeyguardUpdateMonitor keyguardUpdateMonitor, DialogTransitionAnimator dialogTransitionAnimator, SelectedUserInteractor selectedUserInteractor, + UserLogoutInteractor logoutInteractor, GlobalActionsInteractor interactor) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; - mDevicePolicyManager = devicePolicyManager; mLockPatternUtils = lockPatternUtils; mTelephonyListenerManager = telephonyListenerManager; mKeyguardStateController = keyguardStateController; @@ -412,6 +411,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mKeyguardUpdateMonitor = keyguardUpdateMonitor; mDialogTransitionAnimator = dialogTransitionAnimator; mSelectedUserInteractor = selectedUserInteractor; + mLogoutInteractor = logoutInteractor; mInteractor = interactor; // receive broadcasts @@ -639,12 +639,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) { addIfShouldShowAction(tempActions, new ScreenshotAction()); } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { - // TODO(b/206032495): should call mDevicePolicyManager.getLogoutUserId() instead of - // hardcode it to USER_SYSTEM so it properly supports headless system user mode - // (and then call mDevicePolicyManager.clearLogoutUser() after switched) - if (mDevicePolicyManager.isLogoutEnabled() - && currentUser.get() != null - && currentUser.get().id != UserHandle.USER_SYSTEM) { + if (mLogoutInteractor.isLogoutEnabled().getValue()) { addIfShouldShowAction(tempActions, new LogoutAction()); } } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) { @@ -1134,7 +1129,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Add a little delay before executing, to give the dialog a chance to go away before // switching user mHandler.postDelayed(() -> { - mDevicePolicyManager.logoutUser(); + mLogoutInteractor.logOut(); }, mDialogPressDelay); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 8c5a711d6a75..a5595edcbb95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -116,6 +116,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.AlarmTimeout; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.wakelock.SettableWakeLock; @@ -162,6 +163,7 @@ public class KeyguardIndicationController { private final KeyguardLogger mKeyguardLogger; private final UserTracker mUserTracker; private final BouncerMessageInteractor mBouncerMessageInteractor; + private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; @@ -187,6 +189,7 @@ public class KeyguardIndicationController { private final BiometricMessageInteractor mBiometricMessageInteractor; private DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor; private DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; + private final UserLogoutInteractor mUserLogoutInteractor; private String mPersistentUnlockMessage; private String mAlignmentIndication; private boolean mForceIsDismissible; @@ -237,6 +240,13 @@ public class KeyguardIndicationController { showTrustAgentErrorMessage(mTrustAgentErrorMessage); } }; + @VisibleForTesting + final Consumer<Boolean> mIsLogoutEnabledCallback = + (Boolean isLogoutEnabled) -> { + if (mVisible) { + updateDeviceEntryIndication(false); + } + }; private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override public void onScreenTurnedOn() { @@ -299,7 +309,8 @@ public class KeyguardIndicationController { KeyguardInteractor keyguardInteractor, BiometricMessageInteractor biometricMessageInteractor, DeviceEntryFingerprintAuthInteractor deviceEntryFingerprintAuthInteractor, - DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, + UserLogoutInteractor userLogoutInteractor ) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; @@ -331,6 +342,8 @@ public class KeyguardIndicationController { mBiometricMessageInteractor = biometricMessageInteractor; mDeviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor; mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; + mUserLogoutInteractor = userLogoutInteractor; + mFaceAcquiredMessageDeferral = faceHelpMessageDeferral.create(); @@ -418,6 +431,9 @@ public class KeyguardIndicationController { mCoExAcquisitionMsgIdsToShowCallback); collectFlow(mIndicationArea, mDeviceEntryFingerprintAuthInteractor.isEngaged(), mIsFingerprintEngagedCallback); + collectFlow(mIndicationArea, + mUserLogoutInteractor.isLogoutEnabled(), + mIsLogoutEnabledCallback); } /** @@ -744,9 +760,7 @@ public class KeyguardIndicationController { } private void updateLockScreenLogoutView() { - final boolean shouldShowLogout = mKeyguardUpdateMonitor.isLogoutEnabled() - && getCurrentUser() != UserHandle.USER_SYSTEM; - if (shouldShowLogout) { + if (mUserLogoutInteractor.isLogoutEnabled().getValue()) { mRotateTextViewController.updateIndication( INDICATION_TYPE_LOGOUT, new KeyguardIndication.Builder() @@ -760,7 +774,7 @@ public class KeyguardIndicationController { if (mFalsingManager.isFalseTap(LOW_PENALTY)) { return; } - mDevicePolicyManager.logoutUser(); + mUserLogoutInteractor.logOut(); }) .build(), false); @@ -1515,13 +1529,6 @@ public class KeyguardIndicationController { } @Override - public void onLogoutEnabledChanged() { - if (mVisible) { - updateDeviceEntryIndication(false); - } - } - - @Override public void onRequireUnlockForNfc() { showTransientIndication(mContext.getString(R.string.require_unlock_for_nfc)); hideTransientIndicationDelayed(DEFAULT_HIDE_DELAY_MS); 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 493aa8c11b18..f20ce63467f7 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 @@ -19,12 +19,16 @@ package com.android.systemui.user.data.repository import android.annotation.SuppressLint import android.annotation.UserIdInt +import android.app.admin.DevicePolicyManager import android.content.Context +import android.content.IntentFilter import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -38,6 +42,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -49,11 +54,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -100,6 +106,9 @@ interface UserRepository { /** Whether refresh users should be paused. */ var isRefreshUsersPaused: Boolean + /** Whether logout for secondary users is enabled by admin device policy. */ + val isSecondaryUserLogoutEnabled: StateFlow<Boolean> + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ fun refreshUsers() @@ -109,6 +118,9 @@ interface UserRepository { fun isUserSwitcherEnabled(): Boolean + /** Performs logout logout for secondary users. */ + suspend fun logOutSecondaryUser() + /** * Returns the user ID of the "main user" of the device. This user may have access to certain * features which are limited to at most one user. There will never be more than one main user @@ -137,6 +149,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val globalSettings: GlobalSettings, private val tracker: UserTracker, + private val devicePolicyManager: DevicePolicyManager, + private val broadcastDispatcher: BroadcastDispatcher, ) : UserRepository { private val _userSwitcherSettings: StateFlow<UserSwitcherSettingsModel> = @@ -147,7 +161,7 @@ constructor( SETTING_SIMPLE_USER_SWITCHER, Settings.Global.ADD_USERS_WHEN_LOCKED, Settings.Global.USER_SWITCHER_ENABLED, - ), + ) ) .onStart { emit(Unit) } // Forces an initial update. .map { getSettings() } @@ -163,6 +177,7 @@ constructor( override var mainUserId: Int = UserHandle.USER_NULL private set + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL private set @@ -221,12 +236,51 @@ constructor( .stateIn( applicationScope, SharingStarted.Eagerly, - initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus) + initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus), ) } override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + /** Whether the secondary user logout is enabled by the admin device policy. */ + private val isSecondaryUserLogoutSupported: Flow<Boolean> = + broadcastDispatcher + .broadcastFlow( + filter = + IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED) + ) { intent, _ -> + if ( + DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED == intent.action + ) { + Unit + } else { + null + } + } + .filterNotNull() + .onStart { emit(Unit) } + .map { _ -> devicePolicyManager.isLogoutEnabled() } + .flowOn(backgroundDispatcher) + + @SuppressLint("MissingPermission") + override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> = + selectedUser + .flatMapLatestConflated { selectedUser -> + if (selectedUser.isEligibleForLogout()) { + isSecondaryUserLogoutSupported + } else { + flowOf(false) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + @SuppressLint("MissingPermission") + override suspend fun logOutSecondaryUser() { + if (isSecondaryUserLogoutEnabled.value) { + withContext(backgroundDispatcher) { devicePolicyManager.logoutUser() } + } + } + @SuppressLint("MissingPermission") override fun refreshUsers() { applicationScope.launch { @@ -277,10 +331,7 @@ constructor( ) != 0 val isAddUsersFromLockscreen = - globalSettings.getInt( - Settings.Global.ADD_USERS_WHEN_LOCKED, - 0, - ) != 0 + globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0 val isUserSwitcherEnabled = globalSettings.getInt( @@ -309,3 +360,11 @@ constructor( @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } + +fun SelectedUserModel.isEligibleForLogout(): Boolean { + // TODO(b/206032495): should call mDevicePolicyManager.getLogoutUserId() instead of + // hardcode it to USER_SYSTEM so it properly supports headless system user mode + // (and then call mDevicePolicyManager.clearLogoutUser() after switched) + return selectionStatus == SelectionStatus.SELECTION_COMPLETE && + userInfo.id != android.os.UserHandle.USER_SYSTEM +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt new file mode 100644 index 000000000000..154f1dc3e747 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +/** Encapsulates business logic to for the logout. */ +@SysUISingleton +class UserLogoutInteractor +@Inject +constructor( + private val userRepository: UserRepository, + @Application private val applicationScope: CoroutineScope, +) { + val isLogoutEnabled: StateFlow<Boolean> = userRepository.isSecondaryUserLogoutEnabled + + fun logOut() { + if (userRepository.isSecondaryUserLogoutEnabled.value) { + applicationScope.launch { userRepository.logOutSecondaryUser() } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index df50f765349c..24bca70fd41f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.IActivityManager; -import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -80,6 +79,7 @@ import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.RingerModeLiveData; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.settings.FakeGlobalSettings; @@ -106,7 +106,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs; @Mock private AudioManager mAudioManager; - @Mock private DevicePolicyManager mDevicePolicyManager; @Mock private LockPatternUtils mLockPatternUtils; @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private TelephonyListenerManager mTelephonyListenerManager; @@ -140,6 +139,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private DialogTransitionAnimator mDialogTransitionAnimator; @Mock private SelectedUserInteractor mSelectedUserInteractor; + @Mock private UserLogoutInteractor mLogoutInteractor; @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; @@ -166,7 +166,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext, mWindowManagerFuncs, mAudioManager, - mDevicePolicyManager, mLockPatternUtils, mBroadcastDispatcher, mTelephonyListenerManager, @@ -198,6 +197,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mKeyguardUpdateMonitor, mDialogTransitionAnimator, mSelectedUserInteractor, + mLogoutInteractor, mInteractor); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); 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 ed335f9a1834..1808a5f99f4e 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 @@ -29,6 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.yield @@ -67,6 +68,10 @@ class FakeUserRepository @Inject constructor() : UserRepository { ) override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + private val _isSecondaryUserLogoutEnabled = MutableStateFlow<Boolean>(false) + override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> = + _isSecondaryUserLogoutEnabled.asStateFlow() + override var mainUserId: Int = MAIN_USER_ID override var lastSelectedNonGuestUserId: Int = mainUserId @@ -107,6 +112,17 @@ class FakeUserRepository @Inject constructor() : UserRepository { return _userSwitcherSettings.value.isUserSwitcherEnabled } + fun setSecondaryUserLogoutEnabled(logoutEnabled: Boolean) { + _isSecondaryUserLogoutEnabled.value = logoutEnabled + } + + var logOutSecondaryUserCallCount: Int = 0 + private set + + override suspend fun logOutSecondaryUser() { + logOutSecondaryUserCallCount++ + } + fun setUserInfos(infos: List<UserInfo>) { _userInfos.value = infos } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt new file mode 100644 index 000000000000..d06e74468d3e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.user.data.repository.userRepository + +val Kosmos.userLogoutInteractor by + Kosmos.Fixture { + UserLogoutInteractor( + userRepository = userRepository, + applicationScope = applicationCoroutineScope, + ) + } |