diff options
9 files changed, 280 insertions, 48 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 76002d3f9693..40db63d609ba 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -49,6 +49,7 @@ import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController +import com.android.systemui.statusbar.phone.LockscreenWallpaper import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController @@ -301,4 +302,9 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(KeyguardViewConfigurator::class) abstract fun bindKeyguardViewConfigurator(impl: KeyguardViewConfigurator): CoreStartable + + @Binds + @IntoMap + @ClassKey(LockscreenWallpaper::class) + abstract fun bindLockscreenWallpaper(impl: LockscreenWallpaper): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 8b5db28c2bd1..2fffb4718ed7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2180,9 +2180,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { */ @Override public void setLockscreenUser(int newUserId) { - if (mLockscreenWallpaper != null && !mWallpaperManager.isLockscreenLiveWallpaperEnabled()) { - mLockscreenWallpaper.setCurrentUser(newUserId); - } if (mWallpaperSupported) { mWallpaperChangedReceiver.onReceive(mContext, null); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java index c07b5e062d70..b2c39f7e289f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java @@ -40,12 +40,17 @@ import androidx.annotation.NonNull; import com.android.internal.util.IndentingPrintWriter; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.user.data.model.SelectedUserModel; +import com.android.systemui.user.data.model.SelectionStatus; +import com.android.systemui.user.data.repository.UserRepository; +import com.android.systemui.util.kotlin.JavaAdapter; import libcore.io.IoUtils; @@ -59,7 +64,7 @@ import javax.inject.Inject; */ @SysUISingleton public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable, - Dumpable { + Dumpable, CoreStartable { private static final String TAG = "LockscreenWallpaper"; @@ -72,6 +77,8 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen private final WallpaperManager mWallpaperManager; private final KeyguardUpdateMonitor mUpdateMonitor; private final Handler mH; + private final JavaAdapter mJavaAdapter; + private final UserRepository mUserRepository; private boolean mCached; private Bitmap mCache; @@ -88,6 +95,8 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen DumpManager dumpManager, NotificationMediaManager mediaManager, @Main Handler mainHandler, + JavaAdapter javaAdapter, + UserRepository userRepository, UserTracker userTracker) { dumpManager.registerDumpable(getClass().getSimpleName(), this); mWallpaperManager = wallpaperManager; @@ -95,6 +104,8 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen mUpdateMonitor = keyguardUpdateMonitor; mMediaManager = mediaManager; mH = mainHandler; + mJavaAdapter = javaAdapter; + mUserRepository = userRepository; if (iWallpaperManager != null && !mWallpaperManager.isLockscreenLiveWallpaperEnabled()) { // Service is disabled on some devices like Automotive @@ -106,6 +117,14 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen } } + @Override + public void start() { + if (!isLockscreenLiveWallpaperEnabled()) { + mJavaAdapter.alwaysCollectFlow( + mUserRepository.getSelectedUser(), this::setSelectedUser); + } + } + public Bitmap getBitmap() { assertLockscreenLiveWallpaperNotEnabled(); @@ -169,9 +188,15 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen } } - public void setCurrentUser(int user) { + private void setSelectedUser(SelectedUserModel selectedUserModel) { assertLockscreenLiveWallpaperNotEnabled(); + if (selectedUserModel.getSelectionStatus().equals(SelectionStatus.SELECTION_IN_PROGRESS)) { + // Wait until the selection has finished before updating. + return; + } + + int user = selectedUserModel.getUserInfo().id; if (user != mCurrentUserId) { if (mSelectedUser == null || user != mSelectedUser.getIdentifier()) { mCached = false; diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.kt new file mode 100644 index 000000000000..cefd466374c0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.kt @@ -0,0 +1,35 @@ +/* + * 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.data.model + +import android.content.pm.UserInfo + +/** A model for the currently selected user. */ +data class SelectedUserModel( + /** Information about the user. */ + val userInfo: UserInfo, + /** The current status of the selection. */ + val selectionStatus: SelectionStatus, +) + +/** The current status of the selection. */ +enum class SelectionStatus { + /** This user has started being selected but the selection hasn't completed. */ + SELECTION_IN_PROGRESS, + /** The selection of this user has completed. */ + SELECTION_COMPLETE, +} 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 3de75ca2ed87..954765c4581d 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 @@ -33,6 +33,8 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR import com.android.systemui.settings.UserTracker +import com.android.systemui.user.data.model.SelectedUserModel +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 @@ -69,6 +71,9 @@ interface UserRepository { /** List of all users on the device. */ val userInfos: Flow<List<UserInfo>> + /** Information about the currently-selected user, including [UserInfo] and other details. */ + val selectedUser: StateFlow<SelectedUserModel> + /** [UserInfo] of the currently-selected user. */ val selectedUserInfo: Flow<UserInfo> @@ -146,9 +151,6 @@ constructor( private val _userInfos = MutableStateFlow<List<UserInfo>?>(null) override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull() - private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) - override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() - override var mainUserId: Int = UserHandle.USER_NULL private set override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL @@ -174,12 +176,57 @@ constructor( override var isRefreshUsersPaused: Boolean = false init { - observeSelectedUser() if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) { observeUserSwitching() } } + override val selectedUser: StateFlow<SelectedUserModel> = run { + // Some callbacks don't modify the selection status, so maintain the current value. + var currentSelectionStatus = SelectionStatus.SELECTION_COMPLETE + conflatedCallbackFlow { + fun send(selectionStatus: SelectionStatus) { + currentSelectionStatus = selectionStatus + trySendWithFailureLogging( + SelectedUserModel(tracker.userInfo, selectionStatus), + TAG, + ) + } + + val callback = + object : UserTracker.Callback { + override fun onUserChanging(newUser: Int, userContext: Context) { + send(SelectionStatus.SELECTION_IN_PROGRESS) + } + + override fun onUserChanged(newUser: Int, userContext: Context) { + send(SelectionStatus.SELECTION_COMPLETE) + } + + override fun onProfilesChanged(profiles: List<UserInfo>) { + send(currentSelectionStatus) + } + } + + tracker.addCallback(callback, mainDispatcher.asExecutor()) + send(currentSelectionStatus) + + awaitClose { tracker.removeCallback(callback) } + } + .onEach { + if (!it.userInfo.isGuest) { + lastSelectedNonGuestUserId = it.userInfo.id + } + } + .stateIn( + applicationScope, + SharingStarted.Eagerly, + initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus) + ) + } + + override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + override fun refreshUsers() { applicationScope.launch { val result = withContext(backgroundDispatcher) { manager.aliveUsers } @@ -201,7 +248,7 @@ constructor( } override fun getSelectedUserInfo(): UserInfo { - return checkNotNull(_selectedUserInfo.value) + return selectedUser.value.userInfo } override fun isSimpleUserSwitcher(): Boolean { @@ -234,38 +281,6 @@ constructor( .launchIn(applicationScope) } - private fun observeSelectedUser() { - conflatedCallbackFlow { - fun send() { - trySendWithFailureLogging(tracker.userInfo, TAG) - } - - val callback = - object : UserTracker.Callback { - override fun onUserChanging(newUser: Int, userContext: Context) { - send() - } - - override fun onProfilesChanged(profiles: List<UserInfo>) { - send() - } - } - - tracker.addCallback(callback, mainDispatcher.asExecutor()) - send() - - awaitClose { tracker.removeCallback(callback) } - } - .onEach { - if (!it.isGuest) { - lastSelectedNonGuestUserId = it.id - } - - _selectedUserInfo.value = it - } - .launchIn(applicationScope) - } - private suspend fun getSettings(): UserSwitcherSettingsModel { return withContext(backgroundDispatcher) { val isSimpleUserSwitcher = diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt index 45247977283a..3ea8f5412b40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt @@ -84,6 +84,7 @@ class ShadeInteractorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) featureFlags.set(Flags.FACE_AUTH_REFACTOR, false) + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val refreshUsersScheduler = RefreshUsersScheduler( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.kt new file mode 100644 index 000000000000..47671fbadd0a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.kt @@ -0,0 +1,101 @@ +/* + * 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.statusbar.phone + +import android.app.WallpaperManager +import android.content.pm.UserInfo +import android.os.Looper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.model.SelectionStatus +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.utils.os.FakeHandler +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.verify + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class LockscreenWallpaperTest : SysuiTestCase() { + + private lateinit var underTest: LockscreenWallpaper + + private val testScope = TestScope(StandardTestDispatcher()) + private val userRepository = FakeUserRepository() + + private val wallpaperManager: WallpaperManager = mock() + + @Before + fun setUp() { + whenever(wallpaperManager.isLockscreenLiveWallpaperEnabled).thenReturn(false) + whenever(wallpaperManager.isWallpaperSupported).thenReturn(true) + underTest = + LockscreenWallpaper( + /* wallpaperManager= */ wallpaperManager, + /* iWallpaperManager= */ mock(), + /* keyguardUpdateMonitor= */ mock(), + /* dumpManager= */ mock(), + /* mediaManager= */ mock(), + /* mainHandler= */ FakeHandler(Looper.getMainLooper()), + /* javaAdapter= */ JavaAdapter(testScope.backgroundScope), + /* userRepository= */ userRepository, + /* userTracker= */ mock(), + ) + underTest.start() + } + + @Test + fun getBitmap_matchesUserIdFromUserRepo() = + testScope.runTest { + val info = UserInfo(/* id= */ 5, /* name= */ "id5", /* flags= */ 0) + userRepository.setUserInfos(listOf(info)) + userRepository.setSelectedUserInfo(info) + + underTest.bitmap + + verify(wallpaperManager).getWallpaperFile(any(), eq(5)) + } + + @Test + fun getBitmap_usesOldUserIfNewUserInProgress() = + testScope.runTest { + val info5 = UserInfo(/* id= */ 5, /* name= */ "id5", /* flags= */ 0) + val info6 = UserInfo(/* id= */ 6, /* name= */ "id6", /* flags= */ 0) + userRepository.setUserInfos(listOf(info5, info6)) + userRepository.setSelectedUserInfo(info5) + + // WHEN the selection of user 6 is only in progress + userRepository.setSelectedUserInfo( + info6, + selectionStatus = SelectionStatus.SELECTION_IN_PROGRESS + ) + + underTest.bitmap + + // THEN we still use user 5 for wallpaper selection + verify(wallpaperManager).getWallpaperFile(any(), eq(5)) + } +} 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 079fbcd0304c..0c28cbb52831 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 @@ -26,6 +26,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.user.data.model.SelectedUserModel +import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat @@ -224,6 +226,40 @@ class UserRepositoryImplTest : SysuiTestCase() { } @Test + fun userTrackerCallback_updatesSelectionStatus() = runSelfCancelingTest { + underTest = create(this) + var selectedUser: SelectedUserModel? = null + underTest.selectedUser.onEach { selectedUser = it }.launchIn(this) + setUpUsers(count = 2, selectedIndex = 1) + + // WHEN the user is changing + tracker.onUserChanging(userId = 1) + + // THEN the selection status is IN_PROGRESS + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + + // WHEN the user has finished changing + tracker.onUserChanged(userId = 1) + + // THEN the selection status is COMPLETE + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) + + tracker.onProfileChanged() + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) + + setUpUsers(count = 2, selectedIndex = 0) + + tracker.onUserChanging(userId = 0) + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + + // WHEN a profile change occurs while a user is changing + tracker.onProfileChanged() + + // THEN the selection status remains as IN_PROGRESS + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + } + + @Test fun userSwitchingInProgress_registersUserTrackerCallback() = runSelfCancelingTest { underTest = create(this) 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 61e5b5fc27ea..51ee0c00cb0d 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 @@ -19,18 +19,28 @@ package com.android.systemui.user.data.repository import android.content.pm.UserInfo import android.os.UserHandle +import com.android.systemui.user.data.model.SelectedUserModel +import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.model.UserSwitcherSettingsModel 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 { companion object { // User id to represent a non system (human) user id. We presume this is the main user. private const val MAIN_USER_ID = 10 + + private val DEFAULT_SELECTED_USER = 0 + private val DEFAULT_SELECTED_USER_INFO = + UserInfo( + /* id= */ DEFAULT_SELECTED_USER, + /* name= */ "default selected user", + /* flags= */ 0, + ) } private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) @@ -40,8 +50,11 @@ class FakeUserRepository : UserRepository { private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList()) override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow() - private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) - override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + override val selectedUser = + MutableStateFlow( + SelectedUserModel(DEFAULT_SELECTED_USER_INFO, SelectionStatus.SELECTION_COMPLETE) + ) + override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } private val _userSwitchingInProgress = MutableStateFlow(false) override val userSwitchingInProgress: Flow<Boolean> @@ -72,7 +85,7 @@ class FakeUserRepository : UserRepository { } override fun getSelectedUserInfo(): UserInfo { - return checkNotNull(_selectedUserInfo.value) + return selectedUser.value.userInfo } override fun isSimpleUserSwitcher(): Boolean { @@ -87,12 +100,15 @@ class FakeUserRepository : UserRepository { _userInfos.value = infos } - suspend fun setSelectedUserInfo(userInfo: UserInfo) { + suspend fun setSelectedUserInfo( + userInfo: UserInfo, + selectionStatus: SelectionStatus = SelectionStatus.SELECTION_COMPLETE, + ) { check(_userInfos.value.contains(userInfo)) { "Cannot select the following user, it is not in the list of user infos: $userInfo!" } - _selectedUserInfo.value = userInfo + selectedUser.value = SelectedUserModel(userInfo, selectionStatus) yield() } |