[CS] 1/N: Have LockscreenWallpaper listen for user changes directly.

Bug: 277764509
Test: verify lockscreen wallpaper correctly updates after user switch
Test: manual: verified via logging that
LockscreenWallpaper#setSelectedUser is triggered each time
CentralSurfacesImpl#setLockscreenUser is triggered
Test: atest LockscreenWallpaperTest UserRepositoryImplTest

Change-Id: Ibf8e320b9f0658557e6f0268236cee62c23a1ca8
This commit is contained in:
Caitlin Shkuratov
2023-06-26 16:55:27 +00:00
parent 0a5435a553
commit d2609d03b0
9 changed files with 280 additions and 48 deletions

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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 =

View File

@@ -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(

View File

@@ -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))
}
}

View File

@@ -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
@@ -223,6 +225,40 @@ class UserRepositoryImplTest : SysuiTestCase() {
assertThat(selectedUserInfo?.id).isEqualTo(1)
}
@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)

View File

@@ -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()
}