Merge changes from topics "power-repo-interactor", "user-switcher-domain-layer" into tm-qpr-dev
* changes: Wiring into UserSwitcherActivity. User UI layer. User domain layer. User data layer. Adds power repository and interactor. Adds Text to the common module. Prepares UserSwitcherController.
This commit is contained in:
committed by
Android (Google) Code Review
commit
b76e7b1a4f
@@ -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.common.shared.model
|
||||||
|
|
||||||
|
import android.annotation.StringRes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a text, that can either be already [loaded][Text.Loaded] or be a [reference]
|
||||||
|
* [Text.Resource] to a resource.
|
||||||
|
*/
|
||||||
|
sealed class Text {
|
||||||
|
data class Loaded(
|
||||||
|
val text: String?,
|
||||||
|
) : Text()
|
||||||
|
|
||||||
|
data class Resource(
|
||||||
|
@StringRes val res: Int,
|
||||||
|
) : Text()
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.common.ui.binder
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.android.systemui.common.shared.model.Text
|
||||||
|
|
||||||
|
object TextViewBinder {
|
||||||
|
fun bind(view: TextView, viewModel: Text) {
|
||||||
|
view.text =
|
||||||
|
when (viewModel) {
|
||||||
|
is Text.Resource -> view.context.getString(viewModel.res)
|
||||||
|
is Text.Loaded -> viewModel.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,13 +20,18 @@ import com.android.systemui.power.EnhancedEstimates;
|
|||||||
import com.android.systemui.power.EnhancedEstimatesImpl;
|
import com.android.systemui.power.EnhancedEstimatesImpl;
|
||||||
import com.android.systemui.power.PowerNotificationWarnings;
|
import com.android.systemui.power.PowerNotificationWarnings;
|
||||||
import com.android.systemui.power.PowerUI;
|
import com.android.systemui.power.PowerUI;
|
||||||
|
import com.android.systemui.power.data.repository.PowerRepositoryModule;
|
||||||
|
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
|
|
||||||
/** Dagger Module for code in the power package. */
|
/** Dagger Module for code in the power package. */
|
||||||
@Module
|
@Module(
|
||||||
|
includes = {
|
||||||
|
PowerRepositoryModule.class,
|
||||||
|
}
|
||||||
|
)
|
||||||
public interface PowerModule {
|
public interface PowerModule {
|
||||||
/** */
|
/** */
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.power.data.repository
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.PowerManager
|
||||||
|
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
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/** Defines interface for classes that act as source of truth for power-related data. */
|
||||||
|
interface PowerRepository {
|
||||||
|
/** Whether the device is interactive. Starts with the current state. */
|
||||||
|
val isInteractive: Flow<Boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
@SysUISingleton
|
||||||
|
class PowerRepositoryImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
manager: PowerManager,
|
||||||
|
dispatcher: BroadcastDispatcher,
|
||||||
|
) : PowerRepository {
|
||||||
|
|
||||||
|
override val isInteractive: Flow<Boolean> = conflatedCallbackFlow {
|
||||||
|
fun send() {
|
||||||
|
trySendWithFailureLogging(manager.isInteractive, TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.registerReceiver(
|
||||||
|
receiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
send()
|
||||||
|
|
||||||
|
awaitClose { dispatcher.unregisterReceiver(receiver) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PowerRepository"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.power.data.repository
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
|
||||||
|
@Module
|
||||||
|
interface PowerRepositoryModule {
|
||||||
|
@Binds fun bindRepository(impl: PowerRepositoryImpl): PowerRepository
|
||||||
|
}
|
||||||
@@ -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.power.domain.interactor
|
||||||
|
|
||||||
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
import com.android.systemui.power.data.repository.PowerRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/** Hosts business logic for interacting with the power system. */
|
||||||
|
@SysUISingleton
|
||||||
|
class PowerInteractor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
repository: PowerRepository,
|
||||||
|
) {
|
||||||
|
/** Whether the screen is on or off. */
|
||||||
|
val isInteractive: Flow<Boolean> = repository.isInteractive
|
||||||
|
}
|
||||||
@@ -86,6 +86,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog;
|
|||||||
import com.android.systemui.telephony.TelephonyListenerManager;
|
import com.android.systemui.telephony.TelephonyListenerManager;
|
||||||
import com.android.systemui.user.CreateUserActivity;
|
import com.android.systemui.user.CreateUserActivity;
|
||||||
import com.android.systemui.user.data.source.UserRecord;
|
import com.android.systemui.user.data.source.UserRecord;
|
||||||
|
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper;
|
||||||
import com.android.systemui.util.settings.GlobalSettings;
|
import com.android.systemui.util.settings.GlobalSettings;
|
||||||
import com.android.systemui.util.settings.SecureSettings;
|
import com.android.systemui.util.settings.SecureSettings;
|
||||||
|
|
||||||
@@ -100,14 +101,20 @@ import java.util.function.Consumer;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow;
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow;
|
||||||
|
import kotlinx.coroutines.flow.StateFlowKt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps a list of all users on the device for user switching.
|
* Keeps a list of all users on the device for user switching.
|
||||||
*/
|
*/
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
public class UserSwitcherController implements Dumpable {
|
public class UserSwitcherController implements Dumpable {
|
||||||
|
|
||||||
public static final float USER_SWITCH_ENABLED_ALPHA = 1.0f;
|
public static final float USER_SWITCH_ENABLED_ALPHA =
|
||||||
public static final float USER_SWITCH_DISABLED_ALPHA = 0.38f;
|
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA;
|
||||||
|
public static final float USER_SWITCH_DISABLED_ALPHA =
|
||||||
|
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA;
|
||||||
|
|
||||||
private static final String TAG = "UserSwitcherController";
|
private static final String TAG = "UserSwitcherController";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
@@ -155,7 +162,8 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
private boolean mSimpleUserSwitcher;
|
private boolean mSimpleUserSwitcher;
|
||||||
// When false, there won't be any visual affordance to add a new user from the keyguard even if
|
// When false, there won't be any visual affordance to add a new user from the keyguard even if
|
||||||
// the user is unlocked
|
// the user is unlocked
|
||||||
private boolean mAddUsersFromLockScreen;
|
private final MutableStateFlow<Boolean> mAddUsersFromLockScreen =
|
||||||
|
StateFlowKt.MutableStateFlow(false);
|
||||||
private boolean mUserSwitcherEnabled;
|
private boolean mUserSwitcherEnabled;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
boolean mPauseRefreshUsers;
|
boolean mPauseRefreshUsers;
|
||||||
@@ -258,8 +266,11 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
@Override
|
@Override
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
|
mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
|
||||||
mAddUsersFromLockScreen = mGlobalSettings.getIntForUser(
|
mAddUsersFromLockScreen.setValue(
|
||||||
Settings.Global.ADD_USERS_WHEN_LOCKED, 0, UserHandle.USER_SYSTEM) != 0;
|
mGlobalSettings.getIntForUser(
|
||||||
|
Settings.Global.ADD_USERS_WHEN_LOCKED,
|
||||||
|
0,
|
||||||
|
UserHandle.USER_SYSTEM) != 0);
|
||||||
mUserSwitcherEnabled = mGlobalSettings.getIntForUser(
|
mUserSwitcherEnabled = mGlobalSettings.getIntForUser(
|
||||||
Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0;
|
Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0;
|
||||||
refreshUsers(UserHandle.USER_NULL);
|
refreshUsers(UserHandle.USER_NULL);
|
||||||
@@ -323,7 +334,6 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
mForcePictureLoadForUserId.clear();
|
mForcePictureLoadForUserId.clear();
|
||||||
|
|
||||||
final boolean addUsersWhenLocked = mAddUsersFromLockScreen;
|
|
||||||
mBgExecutor.execute(() -> {
|
mBgExecutor.execute(() -> {
|
||||||
List<UserInfo> infos = mUserManager.getAliveUsers();
|
List<UserInfo> infos = mUserManager.getAliveUsers();
|
||||||
if (infos == null) {
|
if (infos == null) {
|
||||||
@@ -434,7 +444,7 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean anyoneCanCreateUsers() {
|
boolean anyoneCanCreateUsers() {
|
||||||
return systemCanCreateUsers() && mAddUsersFromLockScreen;
|
return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean canCreateGuest(boolean hasExistingGuest) {
|
boolean canCreateGuest(boolean hasExistingGuest) {
|
||||||
@@ -450,7 +460,7 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean createIsRestricted() {
|
boolean createIsRestricted() {
|
||||||
return !mAddUsersFromLockScreen;
|
return !mAddUsersFromLockScreen.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean canCreateSupervisedUser() {
|
boolean canCreateSupervisedUser() {
|
||||||
@@ -516,17 +526,48 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that a user has been selected.
|
||||||
|
*
|
||||||
|
* <p>This will trigger the right user journeys to create a guest user, switch users, and/or
|
||||||
|
* navigate to the correct destination.
|
||||||
|
*
|
||||||
|
* <p>If a user with the given ID is not found, this method is a no-op.
|
||||||
|
*
|
||||||
|
* @param userId The ID of the user to switch to.
|
||||||
|
* @param dialogShower An optional {@link DialogShower} in case we need to show dialogs.
|
||||||
|
*/
|
||||||
|
public void onUserSelected(int userId, @Nullable DialogShower dialogShower) {
|
||||||
|
UserRecord userRecord = mUsers.stream()
|
||||||
|
.filter(x -> x.resolveId() == userId)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (userRecord == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserListItemClicked(userRecord, dialogShower);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether it is allowed to add users while the device is locked. */
|
||||||
|
public Flow<Boolean> getAddUsersFromLockScreen() {
|
||||||
|
return mAddUsersFromLockScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the guest user is configured to always be present on the device. */
|
||||||
|
public boolean isGuestUserAutoCreated() {
|
||||||
|
return mGuestUserAutoCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the guest user is currently being reset. */
|
||||||
|
public boolean isGuestUserResetting() {
|
||||||
|
return mGuestIsResetting.get();
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
|
void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
|
||||||
if (record.isGuest && record.info == null) {
|
if (record.isGuest && record.info == null) {
|
||||||
// No guest user. Create one.
|
createAndSwitchToGuestUser(dialogShower);
|
||||||
createGuestAsync(guestId -> {
|
|
||||||
// guestId may be USER_NULL if we haven't reloaded the user list yet.
|
|
||||||
if (guestId != UserHandle.USER_NULL) {
|
|
||||||
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
|
|
||||||
onUserListItemClicked(guestId, record, dialogShower);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (record.isAddUser) {
|
} else if (record.isAddUser) {
|
||||||
showAddUserDialog(dialogShower);
|
showAddUserDialog(dialogShower);
|
||||||
} else if (record.isAddSupervisedUser) {
|
} else if (record.isAddSupervisedUser) {
|
||||||
@@ -604,7 +645,23 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showAddUserDialog(DialogShower dialogShower) {
|
/**
|
||||||
|
* Creates and switches to the guest user.
|
||||||
|
*/
|
||||||
|
public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) {
|
||||||
|
createGuestAsync(guestId -> {
|
||||||
|
// guestId may be USER_NULL if we haven't reloaded the user list yet.
|
||||||
|
if (guestId != UserHandle.USER_NULL) {
|
||||||
|
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
|
||||||
|
onUserListItemClicked(guestId, UserRecord.createForGuest(), dialogShower);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the add user dialog.
|
||||||
|
*/
|
||||||
|
public void showAddUserDialog(@Nullable DialogShower dialogShower) {
|
||||||
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
|
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
|
||||||
mAddUserDialog.cancel();
|
mAddUserDialog.cancel();
|
||||||
}
|
}
|
||||||
@@ -620,7 +677,10 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startSupervisedUserActivity() {
|
/**
|
||||||
|
* Starts an activity to add a supervised user to the device.
|
||||||
|
*/
|
||||||
|
public void startSupervisedUserActivity() {
|
||||||
final Intent intent = new Intent()
|
final Intent intent = new Intent()
|
||||||
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
|
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
|
||||||
.setPackage(mCreateSupervisedUserPackage)
|
.setPackage(mCreateSupervisedUserPackage)
|
||||||
@@ -772,7 +832,7 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
* Removes guest user and switches to target user. The guest must be the current user and its id
|
* Removes guest user and switches to target user. The guest must be the current user and its id
|
||||||
* must be {@code guestUserId}.
|
* must be {@code guestUserId}.
|
||||||
*
|
*
|
||||||
* <p>If {@code targetUserId} is {@link UserHandle.USER_NULL}, then create a new guest user in
|
* <p>If {@code targetUserId} is {@link UserHandle#USER_NULL}, then create a new guest user in
|
||||||
* the foreground, and immediately switch to it. This is used for wiping the current guest and
|
* the foreground, and immediately switch to it. This is used for wiping the current guest and
|
||||||
* replacing it with a new one.
|
* replacing it with a new one.
|
||||||
*
|
*
|
||||||
@@ -782,11 +842,11 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
* <p>If device is configured with {@link
|
* <p>If device is configured with {@link
|
||||||
* com.android.internal.R.bool.config_guestUserAutoCreated}, then after guest user is removed, a
|
* com.android.internal.R.bool.config_guestUserAutoCreated}, then after guest user is removed, a
|
||||||
* new one is created in the background. This has no effect if {@code targetUserId} is {@link
|
* new one is created in the background. This has no effect if {@code targetUserId} is {@link
|
||||||
* UserHandle.USER_NULL}.
|
* UserHandle#USER_NULL}.
|
||||||
*
|
*
|
||||||
* @param guestUserId id of the guest user to remove
|
* @param guestUserId id of the guest user to remove
|
||||||
* @param targetUserId id of the user to switch to after guest is removed. If {@link
|
* @param targetUserId id of the user to switch to after guest is removed. If {@link
|
||||||
* UserHandle.USER_NULL}, then switch immediately to the newly created guest user.
|
* UserHandle#USER_NULL}, then switch immediately to the newly created guest user.
|
||||||
*/
|
*/
|
||||||
public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) {
|
public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) {
|
||||||
UserInfo currentUser = mUserTracker.getUserInfo();
|
UserInfo currentUser = mUserTracker.getUserInfo();
|
||||||
@@ -839,7 +899,7 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
* user.
|
* user.
|
||||||
*
|
*
|
||||||
* @param guestUserId user id of the guest user to exit
|
* @param guestUserId user id of the guest user to exit
|
||||||
* @param targetUserId user id of the guest user to exit, set to UserHandle.USER_NULL when
|
* @param targetUserId user id of the guest user to exit, set to UserHandle#USER_NULL when
|
||||||
* target user id is not known
|
* target user id is not known
|
||||||
* @param forceRemoveGuestOnExit true: remove guest before switching user,
|
* @param forceRemoveGuestOnExit true: remove guest before switching user,
|
||||||
* false: remove guest only if its ephemeral, else keep guest
|
* false: remove guest only if its ephemeral, else keep guest
|
||||||
@@ -952,7 +1012,7 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
* {@link UserManager} to create a new one.
|
* {@link UserManager} to create a new one.
|
||||||
*
|
*
|
||||||
* @return The multi-user user ID of the newly created guest user, or
|
* @return The multi-user user ID of the newly created guest user, or
|
||||||
* {@link UserHandle.USER_NULL} if the guest couldn't be created.
|
* {@link UserHandle#USER_NULL} if the guest couldn't be created.
|
||||||
*/
|
*/
|
||||||
public @UserIdInt int createGuest() {
|
public @UserIdInt int createGuest() {
|
||||||
UserInfo guest;
|
UserInfo guest;
|
||||||
@@ -1062,38 +1122,11 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getName(Context context, UserRecord item) {
|
public String getName(Context context, UserRecord item) {
|
||||||
if (item.isGuest) {
|
return LegacyUserUiHelper.getUserRecordName(
|
||||||
if (item.isCurrent) {
|
context,
|
||||||
return context.getString(
|
item,
|
||||||
com.android.settingslib.R.string.guest_exit_quick_settings_button);
|
mController.isGuestUserAutoCreated(),
|
||||||
} else {
|
mController.isGuestUserResetting());
|
||||||
if (item.info != null) {
|
|
||||||
return context.getString(com.android.internal.R.string.guest_name);
|
|
||||||
} else {
|
|
||||||
if (mController.mGuestUserAutoCreated) {
|
|
||||||
// If mGuestIsResetting=true, we expect the guest user to be created
|
|
||||||
// shortly, so display a "Resetting guest..." as an indicator that we
|
|
||||||
// are busy. Otherwise, if mGuestIsResetting=false, we probably failed
|
|
||||||
// to create a guest at some point. In this case, always show guest
|
|
||||||
// nickname instead of "Add guest" to make it seem as though the device
|
|
||||||
// always has a guest ready for use.
|
|
||||||
return context.getString(
|
|
||||||
mController.mGuestIsResetting.get()
|
|
||||||
? com.android.settingslib.R.string.guest_resetting
|
|
||||||
: com.android.internal.R.string.guest_name);
|
|
||||||
} else {
|
|
||||||
// we always show "guest" as string, instead of "add guest"
|
|
||||||
return context.getString(com.android.internal.R.string.guest_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (item.isAddUser) {
|
|
||||||
return context.getString(com.android.settingslib.R.string.user_add_user);
|
|
||||||
} else if (item.isAddSupervisedUser) {
|
|
||||||
return context.getString(R.string.add_user_supervised);
|
|
||||||
} else {
|
|
||||||
return item.info.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static ColorFilter getDisabledUserAvatarColorFilter() {
|
protected static ColorFilter getDisabledUserAvatarColorFilter() {
|
||||||
@@ -1103,17 +1136,8 @@ public class UserSwitcherController implements Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected static Drawable getIconDrawable(Context context, UserRecord item) {
|
protected static Drawable getIconDrawable(Context context, UserRecord item) {
|
||||||
int iconRes;
|
int iconRes = LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
|
||||||
if (item.isAddUser) {
|
item.isAddUser, item.isGuest, item.isAddSupervisedUser);
|
||||||
iconRes = R.drawable.ic_add;
|
|
||||||
} else if (item.isGuest) {
|
|
||||||
iconRes = R.drawable.ic_account_circle;
|
|
||||||
} else if (item.isAddSupervisedUser) {
|
|
||||||
iconRes = R.drawable.ic_add_supervised_user;
|
|
||||||
} else {
|
|
||||||
iconRes = R.drawable.ic_avatar_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.getDrawable(iconRes);
|
return context.getDrawable(iconRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package com.android.systemui.user;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.android.settingslib.users.EditUserInfoController;
|
import com.android.settingslib.users.EditUserInfoController;
|
||||||
|
import com.android.systemui.user.data.repository.UserRepositoryModule;
|
||||||
|
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
@@ -29,7 +30,11 @@ import dagger.multibindings.IntoMap;
|
|||||||
/**
|
/**
|
||||||
* Dagger module for User related classes.
|
* Dagger module for User related classes.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module(
|
||||||
|
includes = {
|
||||||
|
UserRepositoryModule.class,
|
||||||
|
}
|
||||||
|
)
|
||||||
public abstract class UserModule {
|
public abstract class UserModule {
|
||||||
|
|
||||||
private static final String FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
|
private static final String FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.graphics.drawable.LayerDrawable
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -37,6 +38,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.constraintlayout.helper.widget.Flow
|
import androidx.constraintlayout.helper.widget.Flow
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.internal.util.UserIcons
|
import com.android.internal.util.UserIcons
|
||||||
import com.android.settingslib.Utils
|
import com.android.settingslib.Utils
|
||||||
@@ -44,6 +46,8 @@ import com.android.systemui.Gefingerpoken
|
|||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
import com.android.systemui.classifier.FalsingCollector
|
import com.android.systemui.classifier.FalsingCollector
|
||||||
|
import com.android.systemui.flags.FeatureFlags
|
||||||
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.plugins.FalsingManager
|
import com.android.systemui.plugins.FalsingManager
|
||||||
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
|
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
@@ -52,6 +56,9 @@ import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdap
|
|||||||
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA
|
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA
|
||||||
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA
|
import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA
|
||||||
import com.android.systemui.user.data.source.UserRecord
|
import com.android.systemui.user.data.source.UserRecord
|
||||||
|
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
|
||||||
|
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||||
|
import dagger.Lazy
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
@@ -63,11 +70,12 @@ private const val USER_VIEW = "user_view"
|
|||||||
class UserSwitcherActivity @Inject constructor(
|
class UserSwitcherActivity @Inject constructor(
|
||||||
private val userSwitcherController: UserSwitcherController,
|
private val userSwitcherController: UserSwitcherController,
|
||||||
private val broadcastDispatcher: BroadcastDispatcher,
|
private val broadcastDispatcher: BroadcastDispatcher,
|
||||||
private val layoutInflater: LayoutInflater,
|
|
||||||
private val falsingCollector: FalsingCollector,
|
private val falsingCollector: FalsingCollector,
|
||||||
private val falsingManager: FalsingManager,
|
private val falsingManager: FalsingManager,
|
||||||
private val userManager: UserManager,
|
private val userManager: UserManager,
|
||||||
private val userTracker: UserTracker
|
private val userTracker: UserTracker,
|
||||||
|
private val flags: FeatureFlags,
|
||||||
|
private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
|
||||||
) : ComponentActivity() {
|
) : ComponentActivity() {
|
||||||
|
|
||||||
private lateinit var parent: UserSwitcherRootView
|
private lateinit var parent: UserSwitcherRootView
|
||||||
@@ -93,119 +101,31 @@ class UserSwitcherActivity @Inject constructor(
|
|||||||
false /* isAddSupervisedUser */
|
false /* isAddSupervisedUser */
|
||||||
)
|
)
|
||||||
|
|
||||||
private val adapter = object : BaseUserAdapter(userSwitcherController) {
|
private val adapter: UserAdapter by lazy { UserAdapter() }
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val item = getItem(position)
|
|
||||||
var view = convertView as ViewGroup?
|
|
||||||
if (view == null) {
|
|
||||||
view = layoutInflater.inflate(
|
|
||||||
R.layout.user_switcher_fullscreen_item,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
) as ViewGroup
|
|
||||||
}
|
|
||||||
(view.getChildAt(0) as ImageView).apply {
|
|
||||||
setImageDrawable(getDrawable(item))
|
|
||||||
}
|
|
||||||
(view.getChildAt(1) as TextView).apply {
|
|
||||||
setText(getName(getContext(), item))
|
|
||||||
}
|
|
||||||
|
|
||||||
view.setEnabled(item.isSwitchToEnabled)
|
|
||||||
view.setAlpha(
|
|
||||||
if (view.isEnabled()) {
|
|
||||||
USER_SWITCH_ENABLED_ALPHA
|
|
||||||
} else {
|
|
||||||
USER_SWITCH_DISABLED_ALPHA
|
|
||||||
}
|
|
||||||
)
|
|
||||||
view.setTag(USER_VIEW)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getName(context: Context, item: UserRecord): String {
|
|
||||||
return if (item == manageUserRecord) {
|
|
||||||
getString(R.string.manage_users)
|
|
||||||
} else {
|
|
||||||
super.getName(context, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findUserIcon(item: UserRecord): Drawable {
|
|
||||||
if (item == manageUserRecord) {
|
|
||||||
return getDrawable(R.drawable.ic_manage_users)
|
|
||||||
}
|
|
||||||
if (item.info == null) {
|
|
||||||
return getIconDrawable(this@UserSwitcherActivity, item)
|
|
||||||
}
|
|
||||||
val userIcon = userManager.getUserIcon(item.info.id)
|
|
||||||
if (userIcon != null) {
|
|
||||||
return BitmapDrawable(userIcon)
|
|
||||||
}
|
|
||||||
return UserIcons.getDefaultUserIcon(resources, item.info.id, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTotalUserViews(): Int {
|
|
||||||
return users.count { item ->
|
|
||||||
!doNotRenderUserView(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doNotRenderUserView(item: UserRecord): Boolean {
|
|
||||||
return item.isAddUser ||
|
|
||||||
item.isAddSupervisedUser ||
|
|
||||||
item.isGuest && item.info == null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDrawable(item: UserRecord): Drawable {
|
|
||||||
var drawable = if (item.isGuest) {
|
|
||||||
getDrawable(R.drawable.ic_account_circle)
|
|
||||||
} else {
|
|
||||||
findUserIcon(item)
|
|
||||||
}
|
|
||||||
drawable.mutate()
|
|
||||||
|
|
||||||
if (!item.isCurrent && !item.isSwitchToEnabled) {
|
|
||||||
drawable.setTint(
|
|
||||||
resources.getColor(
|
|
||||||
R.color.kg_user_switcher_restricted_avatar_icon_color,
|
|
||||||
getTheme()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate()
|
|
||||||
as LayerDrawable
|
|
||||||
if (item == userSwitcherController.getCurrentUserRecord()) {
|
|
||||||
(ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
|
|
||||||
val stroke = resources
|
|
||||||
.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
|
|
||||||
val color = Utils.getColorAttrDefaultColor(
|
|
||||||
this@UserSwitcherActivity,
|
|
||||||
com.android.internal.R.attr.colorAccentPrimary
|
|
||||||
)
|
|
||||||
|
|
||||||
setStroke(stroke, color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ld.setDrawableByLayerId(R.id.user_avatar, drawable)
|
|
||||||
return ld
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyDataSetChanged() {
|
|
||||||
super.notifyDataSetChanged()
|
|
||||||
buildUserViews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.user_switcher_fullscreen)
|
setContentView(R.layout.user_switcher_fullscreen)
|
||||||
window.decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||||
|
if (isUsingModernArchitecture()) {
|
||||||
|
Log.d(TAG, "Using modern architecture.")
|
||||||
|
val viewModel = ViewModelProvider(
|
||||||
|
this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
|
||||||
|
UserSwitcherViewBinder.bind(
|
||||||
|
view = requireViewById(R.id.user_switcher_root),
|
||||||
|
viewModel = viewModel,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
falsingCollector = falsingCollector,
|
||||||
|
onFinish = this::finish,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Not using modern architecture.")
|
||||||
|
}
|
||||||
|
|
||||||
parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
|
parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
|
||||||
|
|
||||||
@@ -346,11 +266,18 @@ class UserSwitcherActivity @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
if (isUsingModernArchitecture()) {
|
||||||
|
return super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
if (isUsingModernArchitecture()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
broadcastDispatcher.unregisterReceiver(broadcastReceiver)
|
broadcastDispatcher.unregisterReceiver(broadcastReceiver)
|
||||||
userTracker.removeCallback(userSwitchedCallback)
|
userTracker.removeCallback(userSwitchedCallback)
|
||||||
@@ -376,6 +303,10 @@ class UserSwitcherActivity @Inject constructor(
|
|||||||
return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt()
|
return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isUsingModernArchitecture(): Boolean {
|
||||||
|
return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
|
||||||
|
}
|
||||||
|
|
||||||
private class ItemAdapter(
|
private class ItemAdapter(
|
||||||
val parentContext: Context,
|
val parentContext: Context,
|
||||||
val resource: Int,
|
val resource: Int,
|
||||||
@@ -398,4 +329,114 @@ class UserSwitcherActivity @Inject constructor(
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class UserAdapter : BaseUserAdapter(userSwitcherController) {
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val item = getItem(position)
|
||||||
|
var view = convertView as ViewGroup?
|
||||||
|
if (view == null) {
|
||||||
|
view = layoutInflater.inflate(
|
||||||
|
R.layout.user_switcher_fullscreen_item,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
) as ViewGroup
|
||||||
|
}
|
||||||
|
(view.getChildAt(0) as ImageView).apply {
|
||||||
|
setImageDrawable(getDrawable(item))
|
||||||
|
}
|
||||||
|
(view.getChildAt(1) as TextView).apply {
|
||||||
|
setText(getName(getContext(), item))
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setEnabled(item.isSwitchToEnabled)
|
||||||
|
view.setAlpha(
|
||||||
|
if (view.isEnabled()) {
|
||||||
|
USER_SWITCH_ENABLED_ALPHA
|
||||||
|
} else {
|
||||||
|
USER_SWITCH_DISABLED_ALPHA
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view.setTag(USER_VIEW)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(context: Context, item: UserRecord): String {
|
||||||
|
return if (item == manageUserRecord) {
|
||||||
|
getString(R.string.manage_users)
|
||||||
|
} else {
|
||||||
|
super.getName(context, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findUserIcon(item: UserRecord): Drawable {
|
||||||
|
if (item == manageUserRecord) {
|
||||||
|
return getDrawable(R.drawable.ic_manage_users)
|
||||||
|
}
|
||||||
|
if (item.info == null) {
|
||||||
|
return getIconDrawable(this@UserSwitcherActivity, item)
|
||||||
|
}
|
||||||
|
val userIcon = userManager.getUserIcon(item.info.id)
|
||||||
|
if (userIcon != null) {
|
||||||
|
return BitmapDrawable(userIcon)
|
||||||
|
}
|
||||||
|
return UserIcons.getDefaultUserIcon(resources, item.info.id, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTotalUserViews(): Int {
|
||||||
|
return users.count { item ->
|
||||||
|
!doNotRenderUserView(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doNotRenderUserView(item: UserRecord): Boolean {
|
||||||
|
return item.isAddUser ||
|
||||||
|
item.isAddSupervisedUser ||
|
||||||
|
item.isGuest && item.info == null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDrawable(item: UserRecord): Drawable {
|
||||||
|
var drawable = if (item.isGuest) {
|
||||||
|
getDrawable(R.drawable.ic_account_circle)
|
||||||
|
} else {
|
||||||
|
findUserIcon(item)
|
||||||
|
}
|
||||||
|
drawable.mutate()
|
||||||
|
|
||||||
|
if (!item.isCurrent && !item.isSwitchToEnabled) {
|
||||||
|
drawable.setTint(
|
||||||
|
resources.getColor(
|
||||||
|
R.color.kg_user_switcher_restricted_avatar_icon_color,
|
||||||
|
getTheme()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate()
|
||||||
|
as LayerDrawable
|
||||||
|
if (item == userSwitcherController.getCurrentUserRecord()) {
|
||||||
|
(ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
|
||||||
|
val stroke = resources
|
||||||
|
.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
|
||||||
|
val color = Utils.getColorAttrDefaultColor(
|
||||||
|
this@UserSwitcherActivity,
|
||||||
|
com.android.internal.R.attr.colorAccentPrimary
|
||||||
|
)
|
||||||
|
|
||||||
|
setStroke(stroke, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ld.setDrawableByLayerId(R.id.user_avatar, drawable)
|
||||||
|
return ld
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyDataSetChanged() {
|
||||||
|
super.notifyDataSetChanged()
|
||||||
|
buildUserViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UserSwitcherActivity"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* 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.Context
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.UserManager
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import com.android.internal.util.UserIcons
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
|
||||||
|
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||||
|
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.statusbar.policy.UserSwitcherController
|
||||||
|
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 javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acts as source of truth for user related data.
|
||||||
|
*
|
||||||
|
* Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about
|
||||||
|
* upstream changes.
|
||||||
|
*/
|
||||||
|
interface UserRepository {
|
||||||
|
/** List of all users on the device. */
|
||||||
|
val users: Flow<List<UserModel>>
|
||||||
|
|
||||||
|
/** The currently-selected user. */
|
||||||
|
val selectedUser: Flow<UserModel>
|
||||||
|
|
||||||
|
/** List of available user-related actions. */
|
||||||
|
val actions: Flow<List<UserActionModel>>
|
||||||
|
|
||||||
|
/** Whether actions are available even when locked. */
|
||||||
|
val isActionableWhenLocked: Flow<Boolean>
|
||||||
|
|
||||||
|
/** Whether the device is configured to always have a guest user available. */
|
||||||
|
val isGuestUserAutoCreated: Boolean
|
||||||
|
|
||||||
|
/** Whether the guest user is currently being reset. */
|
||||||
|
val isGuestUserResetting: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@SysUISingleton
|
||||||
|
class UserRepositoryImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@Application private val appContext: Context,
|
||||||
|
private val manager: UserManager,
|
||||||
|
controller: UserSwitcherController,
|
||||||
|
) : UserRepository {
|
||||||
|
|
||||||
|
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
|
||||||
|
fun send() {
|
||||||
|
trySendWithFailureLogging(
|
||||||
|
controller.users,
|
||||||
|
TAG,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val callback = UserSwitcherController.UserSwitchCallback { send() }
|
||||||
|
|
||||||
|
controller.addUserSwitchCallback(callback)
|
||||||
|
send()
|
||||||
|
|
||||||
|
awaitClose { controller.removeUserSwitchCallback(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override val users: Flow<List<UserModel>> =
|
||||||
|
userRecords.map { records -> records.filter { it.isUser() }.map { it.toUserModel() } }
|
||||||
|
|
||||||
|
override val selectedUser: Flow<UserModel> =
|
||||||
|
users.map { users -> users.first { user -> user.isSelected } }
|
||||||
|
|
||||||
|
override val actions: Flow<List<UserActionModel>> =
|
||||||
|
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
|
||||||
|
|
||||||
|
override val isActionableWhenLocked: Flow<Boolean> = controller.addUsersFromLockScreen
|
||||||
|
|
||||||
|
override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
|
||||||
|
|
||||||
|
override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
|
||||||
|
|
||||||
|
private fun UserRecord.isUser(): Boolean {
|
||||||
|
return when {
|
||||||
|
isAddUser -> false
|
||||||
|
isAddSupervisedUser -> false
|
||||||
|
isGuest -> info != null
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UserRecord.isNotUser(): Boolean {
|
||||||
|
return !isUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UserRecord.toUserModel(): UserModel {
|
||||||
|
return UserModel(
|
||||||
|
id = resolveId(),
|
||||||
|
name = getUserName(this),
|
||||||
|
image = getUserImage(this),
|
||||||
|
isSelected = isCurrent,
|
||||||
|
isSelectable = isSwitchToEnabled || isGuest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UserRecord.toActionModel(): UserActionModel {
|
||||||
|
return when {
|
||||||
|
isAddUser -> UserActionModel.ADD_USER
|
||||||
|
isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
|
||||||
|
isGuest -> UserActionModel.ENTER_GUEST_MODE
|
||||||
|
else -> error("Don't know how to convert to UserActionModel: $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserName(record: UserRecord): Text {
|
||||||
|
val resourceId: Int? = LegacyUserUiHelper.getGuestUserRecordNameResourceId(record)
|
||||||
|
return if (resourceId != null) {
|
||||||
|
Text.Resource(resourceId)
|
||||||
|
} else {
|
||||||
|
Text.Loaded(checkNotNull(record.info).name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserImage(record: UserRecord): Drawable {
|
||||||
|
if (record.isGuest) {
|
||||||
|
return checkNotNull(
|
||||||
|
AppCompatResources.getDrawable(appContext, R.drawable.ic_account_circle)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val userId = checkNotNull(record.info?.id)
|
||||||
|
return manager.getUserIcon(userId)?.let { userSelectedIcon ->
|
||||||
|
BitmapDrawable(userSelectedIcon)
|
||||||
|
}
|
||||||
|
?: UserIcons.getDefaultUserIcon(appContext.resources, userId, /* light= */ false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UserRepository"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.user.data.repository
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
|
||||||
|
@Module
|
||||||
|
interface UserRepositoryModule {
|
||||||
|
@Binds fun bindRepository(impl: UserRepositoryImpl): UserRepository
|
||||||
|
}
|
||||||
@@ -20,38 +20,29 @@ import android.content.pm.UserInfo
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
|
|
||||||
/**
|
/** Encapsulates raw data for a user or an option item related to managing users on the device. */
|
||||||
* Encapsulates raw data for a user or an option item related to managing users on the device.
|
|
||||||
*/
|
|
||||||
data class UserRecord(
|
data class UserRecord(
|
||||||
/** Relevant user information. If `null`, this record is not a user but an option item. */
|
/** Relevant user information. If `null`, this record is not a user but an option item. */
|
||||||
@JvmField
|
@JvmField val info: UserInfo? = null,
|
||||||
val info: UserInfo?,
|
|
||||||
/** An image representing the user. */
|
/** An image representing the user. */
|
||||||
@JvmField
|
@JvmField val picture: Bitmap? = null,
|
||||||
val picture: Bitmap?,
|
|
||||||
/** Whether this record represents an option to switch to a guest user. */
|
/** Whether this record represents an option to switch to a guest user. */
|
||||||
@JvmField
|
@JvmField val isGuest: Boolean = false,
|
||||||
val isGuest: Boolean,
|
|
||||||
/** Whether this record represents the currently-selected user. */
|
/** Whether this record represents the currently-selected user. */
|
||||||
@JvmField
|
@JvmField val isCurrent: Boolean = false,
|
||||||
val isCurrent: Boolean,
|
|
||||||
/** Whether this record represents an option to add another user to the device. */
|
/** Whether this record represents an option to add another user to the device. */
|
||||||
@JvmField
|
@JvmField val isAddUser: Boolean = false,
|
||||||
val isAddUser: Boolean,
|
|
||||||
/** If true, the record is only visible to the owner and only when unlocked. */
|
|
||||||
@JvmField
|
|
||||||
val isRestricted: Boolean,
|
|
||||||
/** Whether it is possible to switch to this user. */
|
|
||||||
@JvmField
|
|
||||||
val isSwitchToEnabled: Boolean,
|
|
||||||
/** Whether this record represents an option to add another supervised user to the device. */
|
|
||||||
@JvmField
|
|
||||||
val isAddSupervisedUser: Boolean,
|
|
||||||
) {
|
|
||||||
/**
|
/**
|
||||||
* Returns a new instance of [UserRecord] with its [isCurrent] set to the given value.
|
* If true, the record is only available if unlocked or if the user has granted permission to
|
||||||
|
* access this user action whilst on the device is locked.
|
||||||
*/
|
*/
|
||||||
|
@JvmField val isRestricted: Boolean = false,
|
||||||
|
/** Whether it is possible to switch to this user. */
|
||||||
|
@JvmField val isSwitchToEnabled: Boolean = false,
|
||||||
|
/** Whether this record represents an option to add another supervised user to the device. */
|
||||||
|
@JvmField val isAddSupervisedUser: Boolean = false,
|
||||||
|
) {
|
||||||
|
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
|
||||||
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
|
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
|
||||||
return copy(isCurrent = isCurrent)
|
return copy(isCurrent = isCurrent)
|
||||||
}
|
}
|
||||||
@@ -67,4 +58,11 @@ data class UserRecord(
|
|||||||
info.id
|
info.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun createForGuest(): UserRecord {
|
||||||
|
return UserRecord(isGuest = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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.provider.Settings
|
||||||
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
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.user.data.repository.UserRepository
|
||||||
|
import com.android.systemui.user.shared.model.UserActionModel
|
||||||
|
import com.android.systemui.user.shared.model.UserModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
/** Encapsulates business logic to interact with user data and systems. */
|
||||||
|
@SysUISingleton
|
||||||
|
class UserInteractor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
repository: UserRepository,
|
||||||
|
private val controller: UserSwitcherController,
|
||||||
|
private val activityStarter: ActivityStarter,
|
||||||
|
keyguardInteractor: KeyguardInteractor,
|
||||||
|
) {
|
||||||
|
/** List of current on-device users to select from. */
|
||||||
|
val users: Flow<List<UserModel>> = repository.users
|
||||||
|
|
||||||
|
/** The currently-selected user. */
|
||||||
|
val selectedUser: Flow<UserModel> = repository.selectedUser
|
||||||
|
|
||||||
|
/** List of user-switcher related actions that are available. */
|
||||||
|
val actions: Flow<List<UserActionModel>> =
|
||||||
|
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
|
||||||
|
|
||||||
|
/** Whether the guest user is currently being reset. */
|
||||||
|
val isGuestUserResetting: Boolean = repository.isGuestUserResetting
|
||||||
|
|
||||||
|
/** Switches to the user with the given user ID. */
|
||||||
|
fun selectUser(
|
||||||
|
userId: Int,
|
||||||
|
) {
|
||||||
|
controller.onUserSelected(userId, /* 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.user.data.source.UserRecord
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines utility functions for helping with legacy UI 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 LegacyUserUiHelper {
|
||||||
|
|
||||||
|
/** Returns the maximum number of columns for user items in the user switcher. */
|
||||||
|
fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
|
||||||
|
// TODO(b/243844097): remove this once we remove the old user switcher implementation.
|
||||||
|
return if (userCount < 5) {
|
||||||
|
4
|
||||||
|
} else {
|
||||||
|
ceil(userCount / 2.0).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@DrawableRes
|
||||||
|
fun getUserSwitcherActionIconResourceId(
|
||||||
|
isAddUser: Boolean,
|
||||||
|
isGuest: Boolean,
|
||||||
|
isAddSupervisedUser: Boolean,
|
||||||
|
): Int {
|
||||||
|
return if (isAddUser) {
|
||||||
|
R.drawable.ic_add
|
||||||
|
} else if (isGuest) {
|
||||||
|
R.drawable.ic_account_circle
|
||||||
|
} else if (isAddSupervisedUser) {
|
||||||
|
R.drawable.ic_add_supervised_user
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_avatar_user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getUserRecordName(
|
||||||
|
context: Context,
|
||||||
|
record: UserRecord,
|
||||||
|
isGuestUserAutoCreated: Boolean,
|
||||||
|
isGuestUserResetting: Boolean,
|
||||||
|
): String {
|
||||||
|
val resourceId: Int? = getGuestUserRecordNameResourceId(record)
|
||||||
|
return when {
|
||||||
|
resourceId != null -> context.getString(resourceId)
|
||||||
|
record.info != null -> record.info.name
|
||||||
|
else ->
|
||||||
|
context.getString(
|
||||||
|
getUserSwitcherActionTextResourceId(
|
||||||
|
isGuest = record.isGuest,
|
||||||
|
isGuestUserAutoCreated = isGuestUserAutoCreated,
|
||||||
|
isGuestUserResetting = isGuestUserResetting,
|
||||||
|
isAddUser = record.isAddUser,
|
||||||
|
isAddSupervisedUser = record.isAddSupervisedUser,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for a string for the name of the guest user.
|
||||||
|
*
|
||||||
|
* If the given record is not the guest user, returns `null`.
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
fun getGuestUserRecordNameResourceId(record: UserRecord): Int? {
|
||||||
|
return when {
|
||||||
|
record.isGuest && record.isCurrent ->
|
||||||
|
com.android.settingslib.R.string.guest_exit_quick_settings_button
|
||||||
|
record.isGuest && record.info != null -> com.android.internal.R.string.guest_name
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@StringRes
|
||||||
|
fun getUserSwitcherActionTextResourceId(
|
||||||
|
isGuest: Boolean,
|
||||||
|
isGuestUserAutoCreated: Boolean,
|
||||||
|
isGuestUserResetting: Boolean,
|
||||||
|
isAddUser: Boolean,
|
||||||
|
isAddSupervisedUser: Boolean,
|
||||||
|
): Int {
|
||||||
|
check(isGuest || isAddUser || isAddSupervisedUser)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
|
||||||
|
com.android.settingslib.R.string.guest_resetting
|
||||||
|
isGuest && isGuestUserAutoCreated -> com.android.internal.R.string.guest_name
|
||||||
|
isGuest -> com.android.internal.R.string.guest_name
|
||||||
|
isAddUser -> com.android.settingslib.R.string.user_add_user
|
||||||
|
isAddSupervisedUser -> R.string.add_user_supervised
|
||||||
|
else -> error("This should never happen!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alpha value to apply to a user view in the user switcher when it's selectable. */
|
||||||
|
const val USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA = 1.0f
|
||||||
|
|
||||||
|
/** Alpha value to apply to a user view in the user switcher when it's not selectable. */
|
||||||
|
const val USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA = 0.38f
|
||||||
|
}
|
||||||
@@ -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.shared.model
|
||||||
|
|
||||||
|
enum class UserActionModel {
|
||||||
|
ENTER_GUEST_MODE,
|
||||||
|
ADD_USER,
|
||||||
|
ADD_SUPERVISED_USER,
|
||||||
|
NAVIGATE_TO_USER_MANAGEMENT,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.shared.model
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.android.systemui.common.shared.model.Text
|
||||||
|
|
||||||
|
/** Represents a single user on the device. */
|
||||||
|
data class UserModel(
|
||||||
|
/** ID of the user, unique across all users on this device. */
|
||||||
|
val id: Int,
|
||||||
|
/** Human-facing name for this user. */
|
||||||
|
val name: Text,
|
||||||
|
/** Human-facing image for this user. */
|
||||||
|
val image: Drawable,
|
||||||
|
/** Whether this user is the currently-selected user. */
|
||||||
|
val isSelected: Boolean,
|
||||||
|
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
|
||||||
|
val isSelectable: Boolean,
|
||||||
|
)
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* 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.binder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.helper.widget.Flow as FlowWidget
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.android.systemui.Gefingerpoken
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.classifier.FalsingCollector
|
||||||
|
import com.android.systemui.user.UserSwitcherPopupMenu
|
||||||
|
import com.android.systemui.user.UserSwitcherRootView
|
||||||
|
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
|
||||||
|
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||||
|
import com.android.systemui.util.children
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/** Binds a user switcher to its view-model. */
|
||||||
|
object UserSwitcherViewBinder {
|
||||||
|
|
||||||
|
private const val USER_VIEW_TAG = "user_view"
|
||||||
|
|
||||||
|
/** Binds the given view to the given view-model. */
|
||||||
|
fun bind(
|
||||||
|
view: ViewGroup,
|
||||||
|
viewModel: UserSwitcherViewModel,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
layoutInflater: LayoutInflater,
|
||||||
|
falsingCollector: FalsingCollector,
|
||||||
|
onFinish: () -> Unit,
|
||||||
|
) {
|
||||||
|
val rootView: UserSwitcherRootView = view.requireViewById(R.id.user_switcher_root)
|
||||||
|
val flowWidget: FlowWidget = view.requireViewById(R.id.flow)
|
||||||
|
val addButton: View = view.requireViewById(R.id.add)
|
||||||
|
val cancelButton: View = view.requireViewById(R.id.cancel)
|
||||||
|
val popupMenuAdapter = MenuAdapter(layoutInflater)
|
||||||
|
var popupMenu: UserSwitcherPopupMenu? = null
|
||||||
|
|
||||||
|
rootView.touchHandler =
|
||||||
|
object : Gefingerpoken {
|
||||||
|
override fun onTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
|
falsingCollector.onTouchEvent(ev)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() }
|
||||||
|
cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() }
|
||||||
|
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
launch {
|
||||||
|
viewModel.isFinishRequested
|
||||||
|
.filter { it }
|
||||||
|
.collect {
|
||||||
|
onFinish()
|
||||||
|
viewModel.onFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } }
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.isMenuVisible.collect { isVisible ->
|
||||||
|
if (isVisible && popupMenu?.isShowing != true) {
|
||||||
|
popupMenu?.dismiss()
|
||||||
|
// Use post to make sure we show the popup menu *after* the activity is
|
||||||
|
// ready to show one to avoid a WindowManager$BadTokenException.
|
||||||
|
view.post {
|
||||||
|
popupMenu =
|
||||||
|
createAndShowPopupMenu(
|
||||||
|
context = view.context,
|
||||||
|
anchorView = addButton,
|
||||||
|
adapter = popupMenuAdapter,
|
||||||
|
onDismissed = viewModel::onMenuClosed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!isVisible && popupMenu?.isShowing == true) {
|
||||||
|
popupMenu?.dismiss()
|
||||||
|
popupMenu = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.menu.collect { menuViewModels ->
|
||||||
|
popupMenuAdapter.setItems(menuViewModels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.maximumUserColumns.collect { maximumColumns ->
|
||||||
|
flowWidget.setMaxElementsWrap(maximumColumns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.users.collect { users ->
|
||||||
|
val viewPool =
|
||||||
|
view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList()
|
||||||
|
viewPool.forEach { view.removeView(it) }
|
||||||
|
users.forEach { userViewModel ->
|
||||||
|
val userView =
|
||||||
|
if (viewPool.isNotEmpty()) {
|
||||||
|
viewPool.removeAt(0)
|
||||||
|
} else {
|
||||||
|
val inflatedView =
|
||||||
|
layoutInflater.inflate(
|
||||||
|
R.layout.user_switcher_fullscreen_item,
|
||||||
|
view,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
inflatedView.tag = USER_VIEW_TAG
|
||||||
|
inflatedView
|
||||||
|
}
|
||||||
|
userView.id = View.generateViewId()
|
||||||
|
view.addView(userView)
|
||||||
|
flowWidget.addView(userView)
|
||||||
|
UserViewBinder.bind(
|
||||||
|
view = userView,
|
||||||
|
viewModel = userViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndShowPopupMenu(
|
||||||
|
context: Context,
|
||||||
|
anchorView: View,
|
||||||
|
adapter: MenuAdapter,
|
||||||
|
onDismissed: () -> Unit,
|
||||||
|
): UserSwitcherPopupMenu {
|
||||||
|
return UserSwitcherPopupMenu(context).apply {
|
||||||
|
this.anchorView = anchorView
|
||||||
|
setAdapter(adapter)
|
||||||
|
setOnDismissListener { onDismissed() }
|
||||||
|
setOnItemClickListener { _, _, position, _ ->
|
||||||
|
val itemPositionExcludingHeader = position - 1
|
||||||
|
adapter.getItem(itemPositionExcludingHeader).onClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adapter for the menu that can be opened. */
|
||||||
|
private class MenuAdapter(
|
||||||
|
private val layoutInflater: LayoutInflater,
|
||||||
|
) : BaseAdapter() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<UserActionViewModel>()
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): UserActionViewModel {
|
||||||
|
return items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return getItem(position).viewKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view =
|
||||||
|
convertView
|
||||||
|
?: layoutInflater.inflate(
|
||||||
|
R.layout.user_switcher_fullscreen_popup_item,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
val viewModel = getItem(position)
|
||||||
|
view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId)
|
||||||
|
view.requireViewById<TextView>(R.id.text).text =
|
||||||
|
view.resources.getString(viewModel.textResourceId)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<UserActionViewModel>) {
|
||||||
|
this.items.clear()
|
||||||
|
this.items.addAll(items)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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.binder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import com.android.settingslib.Utils
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.common.ui.binder.TextViewBinder
|
||||||
|
import com.android.systemui.user.ui.viewmodel.UserViewModel
|
||||||
|
|
||||||
|
/** Binds a user view to its view-model. */
|
||||||
|
object UserViewBinder {
|
||||||
|
/** Binds the given view to the given view-model. */
|
||||||
|
fun bind(view: View, viewModel: UserViewModel) {
|
||||||
|
TextViewBinder.bind(view.requireViewById(R.id.user_switcher_text), viewModel.name)
|
||||||
|
view
|
||||||
|
.requireViewById<ImageView>(R.id.user_switcher_icon)
|
||||||
|
.setImageDrawable(getSelectableDrawable(view.context, viewModel))
|
||||||
|
view.alpha = viewModel.alpha
|
||||||
|
if (viewModel.onClicked != null) {
|
||||||
|
view.setOnClickListener { viewModel.onClicked.invoke() }
|
||||||
|
} else {
|
||||||
|
view.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectableDrawable(context: Context, viewModel: UserViewModel): Drawable {
|
||||||
|
val layerDrawable =
|
||||||
|
checkNotNull(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
context.resources,
|
||||||
|
R.drawable.user_switcher_icon_large,
|
||||||
|
context.theme,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.mutate() as LayerDrawable
|
||||||
|
if (viewModel.isSelectionMarkerVisible) {
|
||||||
|
(layerDrawable.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
|
||||||
|
val stroke =
|
||||||
|
context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.user_switcher_icon_selected_width
|
||||||
|
)
|
||||||
|
val color =
|
||||||
|
Utils.getColorAttrDefaultColor(
|
||||||
|
context,
|
||||||
|
com.android.internal.R.attr.colorAccentPrimary
|
||||||
|
)
|
||||||
|
|
||||||
|
setStroke(stroke, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerDrawable.setDrawableByLayerId(R.id.user_avatar, viewModel.image)
|
||||||
|
return layerDrawable
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.viewmodel
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
/** Models UI state for an action that can be performed on a user. */
|
||||||
|
data class UserActionViewModel(
|
||||||
|
/**
|
||||||
|
* Key to use with the view or compose system to keep track of the view/composable across
|
||||||
|
* changes to the collection of [UserActionViewModel] instances.
|
||||||
|
*/
|
||||||
|
val viewKey: Long,
|
||||||
|
@DrawableRes val iconResourceId: Int,
|
||||||
|
@StringRes val textResourceId: Int,
|
||||||
|
val onClicked: () -> Unit,
|
||||||
|
)
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||||
|
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||||
|
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 javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
/** Models UI state for the user switcher feature. */
|
||||||
|
class UserSwitcherViewModel
|
||||||
|
private constructor(
|
||||||
|
private val userInteractor: UserInteractor,
|
||||||
|
private val powerInteractor: PowerInteractor,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/** On-device users. */
|
||||||
|
val users: Flow<List<UserViewModel>> =
|
||||||
|
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
|
||||||
|
|
||||||
|
/** The maximum number of columns that the user selection grid should use. */
|
||||||
|
val maximumUserColumns: Flow<Int> =
|
||||||
|
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
|
||||||
|
|
||||||
|
/** Whether the button to open the user action menu is visible. */
|
||||||
|
val isOpenMenuButtonVisible: Flow<Boolean> = 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
|
||||||
|
* consumer must invoke [onMenuClosed].
|
||||||
|
*/
|
||||||
|
val isMenuVisible: Flow<Boolean> = _isMenuVisible
|
||||||
|
/** The user action menu. */
|
||||||
|
val menu: Flow<List<UserActionViewModel>> =
|
||||||
|
userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
|
||||||
|
|
||||||
|
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
|
||||||
|
* by the consumer.
|
||||||
|
*/
|
||||||
|
val isFinishRequested: Flow<Boolean> = createFinishRequestedFlow()
|
||||||
|
|
||||||
|
/** Notifies that the user has clicked the cancel button. */
|
||||||
|
fun onCancelButtonClicked() {
|
||||||
|
hasCancelButtonBeenClicked.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the user experience is finished.
|
||||||
|
*
|
||||||
|
* Call this after consuming [isFinishRequested] with a `true` value in order to mark it as
|
||||||
|
* consumed such that the next consumer doesn't immediately finish itself.
|
||||||
|
*/
|
||||||
|
fun onFinished() {
|
||||||
|
hasCancelButtonBeenClicked.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notifies that the user has clicked the "open menu" button. */
|
||||||
|
fun onOpenMenuButtonClicked() {
|
||||||
|
_isMenuVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the user has dismissed or closed the user action menu.
|
||||||
|
*
|
||||||
|
* Call this after consuming [isMenuVisible] with a `true` value in order to reset it to `false`
|
||||||
|
* such that the next consumer doesn't immediately show the menu again.
|
||||||
|
*/
|
||||||
|
fun onMenuClosed() {
|
||||||
|
_isMenuVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFinishRequestedFlow(): Flow<Boolean> {
|
||||||
|
var mostRecentSelectedUserId: Int? = null
|
||||||
|
var mostRecentIsInteractive: Boolean? = null
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
// When the user is switched, we should finish.
|
||||||
|
userInteractor.selectedUser
|
||||||
|
.map { it.id }
|
||||||
|
.map {
|
||||||
|
val selectedUserChanged =
|
||||||
|
mostRecentSelectedUserId != null && mostRecentSelectedUserId != it
|
||||||
|
mostRecentSelectedUserId = it
|
||||||
|
selectedUserChanged
|
||||||
|
},
|
||||||
|
// When the screen turns off, we should finish.
|
||||||
|
powerInteractor.isInteractive.map {
|
||||||
|
val screenTurnedOff = mostRecentIsInteractive == true && !it
|
||||||
|
mostRecentIsInteractive = it
|
||||||
|
screenTurnedOff
|
||||||
|
},
|
||||||
|
// When the cancel button is clicked, we should finish.
|
||||||
|
hasCancelButtonBeenClicked,
|
||||||
|
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
|
||||||
|
selectedUserChanged || screenTurnedOff || cancelButtonClicked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toViewModel(
|
||||||
|
model: UserModel,
|
||||||
|
): UserViewModel {
|
||||||
|
return UserViewModel(
|
||||||
|
viewKey = model.id,
|
||||||
|
name = model.name,
|
||||||
|
image = model.image,
|
||||||
|
isSelectionMarkerVisible = model.isSelected,
|
||||||
|
alpha =
|
||||||
|
if (model.isSelectable) {
|
||||||
|
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA
|
||||||
|
} else {
|
||||||
|
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA
|
||||||
|
},
|
||||||
|
onClicked = createOnSelectedCallback(model),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toViewModel(
|
||||||
|
model: UserActionModel,
|
||||||
|
): UserActionViewModel {
|
||||||
|
return UserActionViewModel(
|
||||||
|
viewKey = model.ordinal.toLong(),
|
||||||
|
iconResourceId =
|
||||||
|
if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
|
||||||
|
R.drawable.ic_manage_users
|
||||||
|
} else {
|
||||||
|
LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
|
||||||
|
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
|
||||||
|
isAddUser = model == UserActionModel.ADD_USER,
|
||||||
|
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textResourceId =
|
||||||
|
if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
|
||||||
|
R.string.manage_users
|
||||||
|
} else {
|
||||||
|
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
|
||||||
|
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
|
||||||
|
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
|
||||||
|
isGuestUserResetting = userInteractor.isGuestUserResetting,
|
||||||
|
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
|
||||||
|
isAddUser = model == UserActionModel.ADD_USER,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClicked = { userInteractor.executeAction(action = model) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOnSelectedCallback(model: UserModel): (() -> Unit)? {
|
||||||
|
return if (!model.isSelectable) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
{ userInteractor.selectUser(model.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val userInteractor: UserInteractor,
|
||||||
|
private val powerInteractor: PowerInteractor,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return UserSwitcherViewModel(
|
||||||
|
userInteractor = userInteractor,
|
||||||
|
powerInteractor = powerInteractor,
|
||||||
|
)
|
||||||
|
as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.android.systemui.common.shared.model.Text
|
||||||
|
|
||||||
|
/** Models UI state for representing a single user. */
|
||||||
|
data class UserViewModel(
|
||||||
|
/**
|
||||||
|
* Key to use with the view or compose system to keep track of the view/composable across
|
||||||
|
* changes to the collection of [UserViewModel] instances.
|
||||||
|
*/
|
||||||
|
val viewKey: Int,
|
||||||
|
val name: Text,
|
||||||
|
val image: Drawable,
|
||||||
|
/** Whether a marker should be shown to highlight that this user is the selected one. */
|
||||||
|
val isSelectionMarkerVisible: Boolean,
|
||||||
|
val alpha: Float,
|
||||||
|
val onClicked: (() -> Unit)?,
|
||||||
|
)
|
||||||
@@ -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.power.data.repository
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class FakePowerRepository(
|
||||||
|
initialInteractive: Boolean = true,
|
||||||
|
) : PowerRepository {
|
||||||
|
|
||||||
|
private val _isInteractive = MutableStateFlow(initialInteractive)
|
||||||
|
override val isInteractive: Flow<Boolean> = _isInteractive.asStateFlow()
|
||||||
|
|
||||||
|
fun setInteractive(value: Boolean) {
|
||||||
|
_isInteractive.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
* 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.power.data.repository
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
|
import com.android.systemui.util.mockito.capture
|
||||||
|
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.ArgumentCaptor
|
||||||
|
import org.mockito.Captor
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.anyInt
|
||||||
|
import org.mockito.Mockito.isNull
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class PowerRepositoryImplTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
@Mock private lateinit var manager: PowerManager
|
||||||
|
@Mock private lateinit var dispatcher: BroadcastDispatcher
|
||||||
|
@Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
|
||||||
|
@Captor private lateinit var filterCaptor: ArgumentCaptor<IntentFilter>
|
||||||
|
|
||||||
|
private lateinit var underTest: PowerRepositoryImpl
|
||||||
|
|
||||||
|
private var isInteractive = true
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
isInteractive = true
|
||||||
|
whenever(manager.isInteractive).then { isInteractive }
|
||||||
|
|
||||||
|
underTest = PowerRepositoryImpl(manager = manager, dispatcher = dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - registers for broadcasts`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
val job = underTest.isInteractive.onEach {}.launchIn(this)
|
||||||
|
|
||||||
|
verifyRegistered()
|
||||||
|
assertThat(filterCaptor.value.hasAction(Intent.ACTION_SCREEN_ON)).isTrue()
|
||||||
|
assertThat(filterCaptor.value.hasAction(Intent.ACTION_SCREEN_OFF)).isTrue()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - unregisters from broadcasts`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
val job = underTest.isInteractive.onEach {}.launchIn(this)
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
|
||||||
|
verify(dispatcher).unregisterReceiver(receiverCaptor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - emits initial true value if screen was on`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
isInteractive = true
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
assertThat(value).isTrue()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - emits initial false value if screen was off`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
isInteractive = false
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
assertThat(value).isFalse()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - emits true when the screen turns on`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
isInteractive = true
|
||||||
|
receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_ON))
|
||||||
|
|
||||||
|
assertThat(value).isTrue()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - emits false when the screen turns off`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
isInteractive = false
|
||||||
|
receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF))
|
||||||
|
|
||||||
|
assertThat(value).isFalse()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - emits correctly over time`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
val values = mutableListOf<Boolean>()
|
||||||
|
val job = underTest.isInteractive.onEach(values::add).launchIn(this)
|
||||||
|
verifyRegistered()
|
||||||
|
|
||||||
|
isInteractive = false
|
||||||
|
receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF))
|
||||||
|
isInteractive = true
|
||||||
|
receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_ON))
|
||||||
|
isInteractive = false
|
||||||
|
receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF))
|
||||||
|
|
||||||
|
assertThat(values).isEqualTo(listOf(true, false, true, false))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyRegistered() {
|
||||||
|
// We must verify with all arguments, even those that are optional because they have default
|
||||||
|
// values because Mockito is forcing us to. Once we can use mockito-kotlin, we should be
|
||||||
|
// able to remove this.
|
||||||
|
verify(dispatcher)
|
||||||
|
.registerReceiver(
|
||||||
|
capture(receiverCaptor),
|
||||||
|
capture(filterCaptor),
|
||||||
|
isNull(),
|
||||||
|
isNull(),
|
||||||
|
anyInt(),
|
||||||
|
isNull(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.power.domain.interactor
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.power.data.repository.FakePowerRepository
|
||||||
|
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
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class PowerInteractorTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
private lateinit var underTest: PowerInteractor
|
||||||
|
private lateinit var repository: FakePowerRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
repository =
|
||||||
|
FakePowerRepository(
|
||||||
|
initialInteractive = true,
|
||||||
|
)
|
||||||
|
underTest = PowerInteractor(repository = repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - screen turns off`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
repository.setInteractive(true)
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
repository.setInteractive(false)
|
||||||
|
|
||||||
|
assertThat(value).isFalse()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isInteractive - becomes interactive`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
repository.setInteractive(false)
|
||||||
|
var value: Boolean? = null
|
||||||
|
val job = underTest.isInteractive.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
repository.setInteractive(true)
|
||||||
|
|
||||||
|
assertThat(value).isTrue()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,6 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var mUserDetailItemView: UserDetailItemView
|
@Mock private lateinit var mUserDetailItemView: UserDetailItemView
|
||||||
@Mock private lateinit var mOtherView: View
|
@Mock private lateinit var mOtherView: View
|
||||||
@Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
|
@Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
|
||||||
@Mock private lateinit var mUserInfo: UserInfo
|
|
||||||
@Mock private lateinit var mLayoutInflater: LayoutInflater
|
@Mock private lateinit var mLayoutInflater: LayoutInflater
|
||||||
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
|
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
|
||||||
private lateinit var adapter: UserDetailView.Adapter
|
private lateinit var adapter: UserDetailView.Adapter
|
||||||
@@ -142,7 +141,7 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
|
|||||||
|
|
||||||
private fun createUserRecord(current: Boolean, guest: Boolean) =
|
private fun createUserRecord(current: Boolean, guest: Boolean) =
|
||||||
UserRecord(
|
UserRecord(
|
||||||
mUserInfo,
|
UserInfo(0 /* id */, "name", 0 /* flags */),
|
||||||
mPicture,
|
mPicture,
|
||||||
guest,
|
guest,
|
||||||
current,
|
current,
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var inflatedUserDetailItemView: KeyguardUserDetailItemView
|
private lateinit var inflatedUserDetailItemView: KeyguardUserDetailItemView
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var userInfo: UserInfo
|
|
||||||
@Mock
|
|
||||||
private lateinit var layoutInflater: LayoutInflater
|
private lateinit var layoutInflater: LayoutInflater
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var keyguardUserSwitcherController: KeyguardUserSwitcherController
|
private lateinit var keyguardUserSwitcherController: KeyguardUserSwitcherController
|
||||||
@@ -188,7 +186,7 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
|
|||||||
|
|
||||||
private fun createUserRecord(isCurrentUser: Boolean, isGuestUser: Boolean) =
|
private fun createUserRecord(isCurrentUser: Boolean, isGuestUser: Boolean) =
|
||||||
UserRecord(
|
UserRecord(
|
||||||
userInfo,
|
UserInfo(0 /* id */, "name", 0 /* flags */),
|
||||||
picture,
|
picture,
|
||||||
isGuestUser,
|
isGuestUser,
|
||||||
isCurrentUser,
|
isCurrentUser,
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import androidx.test.filters.SmallTest
|
|||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
import com.android.systemui.classifier.FalsingCollector
|
import com.android.systemui.classifier.FalsingCollector
|
||||||
|
import com.android.systemui.flags.FeatureFlags
|
||||||
import com.android.systemui.plugins.FalsingManager
|
import com.android.systemui.plugins.FalsingManager
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||||
|
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -54,6 +56,10 @@ class UserSwitcherActivityTest : SysuiTestCase() {
|
|||||||
private lateinit var userManager: UserManager
|
private lateinit var userManager: UserManager
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var userTracker: UserTracker
|
private lateinit var userTracker: UserTracker
|
||||||
|
@Mock
|
||||||
|
private lateinit var flags: FeatureFlags
|
||||||
|
@Mock
|
||||||
|
private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory>
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
@@ -61,11 +67,12 @@ class UserSwitcherActivityTest : SysuiTestCase() {
|
|||||||
activity = UserSwitcherActivity(
|
activity = UserSwitcherActivity(
|
||||||
userSwitcherController,
|
userSwitcherController,
|
||||||
broadcastDispatcher,
|
broadcastDispatcher,
|
||||||
layoutInflater,
|
|
||||||
falsingCollector,
|
falsingCollector,
|
||||||
falsingManager,
|
falsingManager,
|
||||||
userManager,
|
userManager,
|
||||||
userTracker
|
userTracker,
|
||||||
|
flags,
|
||||||
|
viewModelFactoryLazy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.user.data.repository
|
||||||
|
|
||||||
|
import com.android.systemui.user.shared.model.UserActionModel
|
||||||
|
import com.android.systemui.user.shared.model.UserModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class FakeUserRepository : UserRepository {
|
||||||
|
|
||||||
|
private val _users = MutableStateFlow<List<UserModel>>(emptyList())
|
||||||
|
override val users: Flow<List<UserModel>> = _users.asStateFlow()
|
||||||
|
override val selectedUser: Flow<UserModel> =
|
||||||
|
users.map { models -> models.first { model -> model.isSelected } }
|
||||||
|
|
||||||
|
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
|
||||||
|
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
|
||||||
|
|
||||||
|
private val _isActionableWhenLocked = MutableStateFlow(false)
|
||||||
|
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
|
||||||
|
|
||||||
|
private var _isGuestUserAutoCreated: Boolean = false
|
||||||
|
override val isGuestUserAutoCreated: Boolean
|
||||||
|
get() = _isGuestUserAutoCreated
|
||||||
|
private var _isGuestUserResetting: Boolean = false
|
||||||
|
override val isGuestUserResetting: Boolean
|
||||||
|
get() = _isGuestUserResetting
|
||||||
|
|
||||||
|
fun setUsers(models: List<UserModel>) {
|
||||||
|
_users.value = models
|
||||||
|
}
|
||||||
|
|
||||||
|
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!"
|
||||||
|
}
|
||||||
|
|
||||||
|
setUsers(
|
||||||
|
_users.value.map { model ->
|
||||||
|
when {
|
||||||
|
model.isSelected && model.id != userId -> model.copy(isSelected = false)
|
||||||
|
!model.isSelected && model.id == userId -> model.copy(isSelected = true)
|
||||||
|
else -> model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActions(models: List<UserActionModel>) {
|
||||||
|
_actions.value = models
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActionableWhenLocked(value: Boolean) {
|
||||||
|
_isActionableWhenLocked.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGuestUserAutoCreated(value: Boolean) {
|
||||||
|
_isGuestUserAutoCreated = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGuestUserResetting(value: Boolean) {
|
||||||
|
_isGuestUserResetting = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* 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.UserManager
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
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.Mock
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class UserRepositoryImplTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
@Mock private lateinit var manager: UserManager
|
||||||
|
@Mock private lateinit var controller: UserSwitcherController
|
||||||
|
@Captor
|
||||||
|
private lateinit var userSwitchCallbackCaptor:
|
||||||
|
ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
|
||||||
|
|
||||||
|
private lateinit var underTest: UserRepositoryImpl
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
whenever(controller.addUsersFromLockScreen).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<UserModel>? = 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 `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<UserActionModel>? = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* 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.keyguard.data.repository.FakeKeyguardRepository
|
||||||
|
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.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 org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.anyBoolean
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class UserInteractorTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
@Mock private lateinit var controller: UserSwitcherController
|
||||||
|
@Mock private lateinit var activityStarter: ActivityStarter
|
||||||
|
|
||||||
|
private lateinit var underTest: UserInteractor
|
||||||
|
|
||||||
|
private lateinit var userRepository: FakeUserRepository
|
||||||
|
private lateinit var keyguardRepository: FakeKeyguardRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
|
userRepository = FakeUserRepository()
|
||||||
|
keyguardRepository = FakeKeyguardRepository()
|
||||||
|
underTest =
|
||||||
|
UserInteractor(
|
||||||
|
repository = userRepository,
|
||||||
|
controller = controller,
|
||||||
|
activityStarter = activityStarter,
|
||||||
|
keyguardInteractor =
|
||||||
|
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<UserActionModel>? = 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<UserActionModel>? = 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<UserActionModel>? = 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<UserActionModel>? = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.common.shared.model.Text
|
||||||
|
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.UserSwitcherController
|
||||||
|
import com.android.systemui.user.data.repository.FakeUserRepository
|
||||||
|
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||||
|
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.mockito.mock
|
||||||
|
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 kotlinx.coroutines.yield
|
||||||
|
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.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
@Mock private lateinit var controller: UserSwitcherController
|
||||||
|
@Mock private lateinit var activityStarter: ActivityStarter
|
||||||
|
|
||||||
|
private lateinit var underTest: UserSwitcherViewModel
|
||||||
|
|
||||||
|
private lateinit var userRepository: FakeUserRepository
|
||||||
|
private lateinit var keyguardRepository: FakeKeyguardRepository
|
||||||
|
private lateinit var powerRepository: FakePowerRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
|
userRepository = FakeUserRepository()
|
||||||
|
keyguardRepository = FakeKeyguardRepository()
|
||||||
|
powerRepository = FakePowerRepository()
|
||||||
|
underTest =
|
||||||
|
UserSwitcherViewModel.Factory(
|
||||||
|
userInteractor =
|
||||||
|
UserInteractor(
|
||||||
|
repository = userRepository,
|
||||||
|
controller = controller,
|
||||||
|
activityStarter = activityStarter,
|
||||||
|
keyguardInteractor =
|
||||||
|
KeyguardInteractor(
|
||||||
|
repository = keyguardRepository,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
powerInteractor =
|
||||||
|
PowerInteractor(
|
||||||
|
repository = powerRepository,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.create(UserSwitcherViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun users() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
userRepository.setUsers(
|
||||||
|
listOf(
|
||||||
|
UserModel(
|
||||||
|
id = 0,
|
||||||
|
name = Text.Loaded("zero"),
|
||||||
|
image = USER_IMAGE,
|
||||||
|
isSelected = true,
|
||||||
|
isSelectable = true,
|
||||||
|
),
|
||||||
|
UserModel(
|
||||||
|
id = 1,
|
||||||
|
name = Text.Loaded("one"),
|
||||||
|
image = USER_IMAGE,
|
||||||
|
isSelected = false,
|
||||||
|
isSelectable = true,
|
||||||
|
),
|
||||||
|
UserModel(
|
||||||
|
id = 2,
|
||||||
|
name = Text.Loaded("two"),
|
||||||
|
image = USER_IMAGE,
|
||||||
|
isSelected = false,
|
||||||
|
isSelectable = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var userViewModels: List<UserViewModel>? = null
|
||||||
|
val job = underTest.users.onEach { userViewModels = it }.launchIn(this)
|
||||||
|
|
||||||
|
assertThat(userViewModels).hasSize(3)
|
||||||
|
assertUserViewModel(
|
||||||
|
viewModel = userViewModels?.get(0),
|
||||||
|
viewKey = 0,
|
||||||
|
name = "zero",
|
||||||
|
isSelectionMarkerVisible = true,
|
||||||
|
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
|
||||||
|
isClickable = true,
|
||||||
|
)
|
||||||
|
assertUserViewModel(
|
||||||
|
viewModel = userViewModels?.get(1),
|
||||||
|
viewKey = 1,
|
||||||
|
name = "one",
|
||||||
|
isSelectionMarkerVisible = false,
|
||||||
|
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
|
||||||
|
isClickable = true,
|
||||||
|
)
|
||||||
|
assertUserViewModel(
|
||||||
|
viewModel = userViewModels?.get(2),
|
||||||
|
viewKey = 2,
|
||||||
|
name = "two",
|
||||||
|
isSelectionMarkerVisible = false,
|
||||||
|
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA,
|
||||||
|
isClickable = false,
|
||||||
|
)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `maximumUserColumns - few users`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
setUsers(count = 2)
|
||||||
|
var value: Int? = null
|
||||||
|
val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(4)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `maximumUserColumns - many users`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
setUsers(count = 5)
|
||||||
|
var value: Int? = null
|
||||||
|
val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(3)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isOpenMenuButtonVisible - has actions - true`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
userRepository.setActions(UserActionModel.values().toList())
|
||||||
|
|
||||||
|
var isVisible: Boolean? = null
|
||||||
|
val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
|
||||||
|
|
||||||
|
assertThat(isVisible).isTrue()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isOpenMenuButtonVisible - no actions - false`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
userRepository.setActions(emptyList())
|
||||||
|
|
||||||
|
var isVisible: Boolean? = null
|
||||||
|
val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
|
||||||
|
|
||||||
|
assertThat(isVisible).isFalse()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun menu() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
userRepository.setActions(UserActionModel.values().toList())
|
||||||
|
var isMenuVisible: Boolean? = null
|
||||||
|
val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this)
|
||||||
|
assertThat(isMenuVisible).isFalse()
|
||||||
|
|
||||||
|
underTest.onOpenMenuButtonClicked()
|
||||||
|
assertThat(isMenuVisible).isTrue()
|
||||||
|
|
||||||
|
underTest.onMenuClosed()
|
||||||
|
assertThat(isMenuVisible).isFalse()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isFinishRequested - finishes when user is switched`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
setUsers(count = 2)
|
||||||
|
var isFinishRequested: Boolean? = null
|
||||||
|
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
|
||||||
|
assertThat(isFinishRequested).isFalse()
|
||||||
|
|
||||||
|
userRepository.setSelectedUser(1)
|
||||||
|
yield()
|
||||||
|
assertThat(isFinishRequested).isTrue()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isFinishRequested - finishes when the screen turns off`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
setUsers(count = 2)
|
||||||
|
powerRepository.setInteractive(true)
|
||||||
|
var isFinishRequested: Boolean? = null
|
||||||
|
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
|
||||||
|
assertThat(isFinishRequested).isFalse()
|
||||||
|
|
||||||
|
powerRepository.setInteractive(false)
|
||||||
|
yield()
|
||||||
|
assertThat(isFinishRequested).isTrue()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isFinishRequested - finishes when cancel button is clicked`() =
|
||||||
|
runBlocking(IMMEDIATE) {
|
||||||
|
setUsers(count = 2)
|
||||||
|
powerRepository.setInteractive(true)
|
||||||
|
var isFinishRequested: Boolean? = null
|
||||||
|
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
|
||||||
|
assertThat(isFinishRequested).isFalse()
|
||||||
|
|
||||||
|
underTest.onCancelButtonClicked()
|
||||||
|
yield()
|
||||||
|
assertThat(isFinishRequested).isTrue()
|
||||||
|
|
||||||
|
underTest.onFinished()
|
||||||
|
yield()
|
||||||
|
assertThat(isFinishRequested).isFalse()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUsers(count: Int) {
|
||||||
|
userRepository.setUsers(
|
||||||
|
(0 until count).map { index ->
|
||||||
|
UserModel(
|
||||||
|
id = index,
|
||||||
|
name = Text.Loaded("$index"),
|
||||||
|
image = USER_IMAGE,
|
||||||
|
isSelected = index == 0,
|
||||||
|
isSelectable = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertUserViewModel(
|
||||||
|
viewModel: UserViewModel?,
|
||||||
|
viewKey: Int,
|
||||||
|
name: String,
|
||||||
|
isSelectionMarkerVisible: Boolean,
|
||||||
|
alpha: Float,
|
||||||
|
isClickable: Boolean,
|
||||||
|
) {
|
||||||
|
checkNotNull(viewModel)
|
||||||
|
assertThat(viewModel.viewKey).isEqualTo(viewKey)
|
||||||
|
assertThat(viewModel.name).isEqualTo(Text.Loaded(name))
|
||||||
|
assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible)
|
||||||
|
assertThat(viewModel.alpha).isEqualTo(alpha)
|
||||||
|
assertThat(viewModel.onClicked != null).isEqualTo(isClickable)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||||
|
private val USER_IMAGE = mock<Drawable>()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user