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.PowerNotificationWarnings;
|
||||
import com.android.systemui.power.PowerUI;
|
||||
import com.android.systemui.power.data.repository.PowerRepositoryModule;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
|
||||
|
||||
/** Dagger Module for code in the power package. */
|
||||
@Module
|
||||
@Module(
|
||||
includes = {
|
||||
PowerRepositoryModule.class,
|
||||
}
|
||||
)
|
||||
public interface PowerModule {
|
||||
/** */
|
||||
@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.user.CreateUserActivity;
|
||||
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.SecureSettings;
|
||||
|
||||
@@ -100,14 +101,20 @@ import java.util.function.Consumer;
|
||||
|
||||
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.
|
||||
*/
|
||||
@SysUISingleton
|
||||
public class UserSwitcherController implements Dumpable {
|
||||
|
||||
public static final float USER_SWITCH_ENABLED_ALPHA = 1.0f;
|
||||
public static final float USER_SWITCH_DISABLED_ALPHA = 0.38f;
|
||||
public static final float USER_SWITCH_ENABLED_ALPHA =
|
||||
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 boolean DEBUG = false;
|
||||
@@ -155,7 +162,8 @@ public class UserSwitcherController implements Dumpable {
|
||||
private boolean mSimpleUserSwitcher;
|
||||
// When false, there won't be any visual affordance to add a new user from the keyguard even if
|
||||
// the user is unlocked
|
||||
private boolean mAddUsersFromLockScreen;
|
||||
private final MutableStateFlow<Boolean> mAddUsersFromLockScreen =
|
||||
StateFlowKt.MutableStateFlow(false);
|
||||
private boolean mUserSwitcherEnabled;
|
||||
@VisibleForTesting
|
||||
boolean mPauseRefreshUsers;
|
||||
@@ -258,8 +266,11 @@ public class UserSwitcherController implements Dumpable {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
|
||||
mAddUsersFromLockScreen = mGlobalSettings.getIntForUser(
|
||||
Settings.Global.ADD_USERS_WHEN_LOCKED, 0, UserHandle.USER_SYSTEM) != 0;
|
||||
mAddUsersFromLockScreen.setValue(
|
||||
mGlobalSettings.getIntForUser(
|
||||
Settings.Global.ADD_USERS_WHEN_LOCKED,
|
||||
0,
|
||||
UserHandle.USER_SYSTEM) != 0);
|
||||
mUserSwitcherEnabled = mGlobalSettings.getIntForUser(
|
||||
Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0;
|
||||
refreshUsers(UserHandle.USER_NULL);
|
||||
@@ -323,7 +334,6 @@ public class UserSwitcherController implements Dumpable {
|
||||
}
|
||||
mForcePictureLoadForUserId.clear();
|
||||
|
||||
final boolean addUsersWhenLocked = mAddUsersFromLockScreen;
|
||||
mBgExecutor.execute(() -> {
|
||||
List<UserInfo> infos = mUserManager.getAliveUsers();
|
||||
if (infos == null) {
|
||||
@@ -434,7 +444,7 @@ public class UserSwitcherController implements Dumpable {
|
||||
}
|
||||
|
||||
boolean anyoneCanCreateUsers() {
|
||||
return systemCanCreateUsers() && mAddUsersFromLockScreen;
|
||||
return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue();
|
||||
}
|
||||
|
||||
boolean canCreateGuest(boolean hasExistingGuest) {
|
||||
@@ -450,7 +460,7 @@ public class UserSwitcherController implements Dumpable {
|
||||
}
|
||||
|
||||
boolean createIsRestricted() {
|
||||
return !mAddUsersFromLockScreen;
|
||||
return !mAddUsersFromLockScreen.getValue();
|
||||
}
|
||||
|
||||
boolean canCreateSupervisedUser() {
|
||||
@@ -516,17 +526,48 @@ public class UserSwitcherController implements Dumpable {
|
||||
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
|
||||
void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
|
||||
if (record.isGuest && record.info == null) {
|
||||
// No guest user. Create one.
|
||||
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);
|
||||
}
|
||||
});
|
||||
createAndSwitchToGuestUser(dialogShower);
|
||||
} else if (record.isAddUser) {
|
||||
showAddUserDialog(dialogShower);
|
||||
} 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()) {
|
||||
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()
|
||||
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
|
||||
.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
|
||||
* 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
|
||||
* replacing it with a new one.
|
||||
*
|
||||
@@ -782,11 +842,11 @@ public class UserSwitcherController implements Dumpable {
|
||||
* <p>If device is configured with {@link
|
||||
* 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
|
||||
* UserHandle.USER_NULL}.
|
||||
* UserHandle#USER_NULL}.
|
||||
*
|
||||
* @param guestUserId id of the guest user to remove
|
||||
* @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) {
|
||||
UserInfo currentUser = mUserTracker.getUserInfo();
|
||||
@@ -839,7 +899,7 @@ public class UserSwitcherController implements Dumpable {
|
||||
* user.
|
||||
*
|
||||
* @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
|
||||
* @param forceRemoveGuestOnExit true: remove guest before switching user,
|
||||
* 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.
|
||||
*
|
||||
* @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() {
|
||||
UserInfo guest;
|
||||
@@ -1062,38 +1122,11 @@ public class UserSwitcherController implements Dumpable {
|
||||
}
|
||||
|
||||
public String getName(Context context, UserRecord item) {
|
||||
if (item.isGuest) {
|
||||
if (item.isCurrent) {
|
||||
return context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_quick_settings_button);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
return LegacyUserUiHelper.getUserRecordName(
|
||||
context,
|
||||
item,
|
||||
mController.isGuestUserAutoCreated(),
|
||||
mController.isGuestUserResetting());
|
||||
}
|
||||
|
||||
protected static ColorFilter getDisabledUserAvatarColorFilter() {
|
||||
@@ -1103,17 +1136,8 @@ public class UserSwitcherController implements Dumpable {
|
||||
}
|
||||
|
||||
protected static Drawable getIconDrawable(Context context, UserRecord item) {
|
||||
int iconRes;
|
||||
if (item.isAddUser) {
|
||||
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;
|
||||
}
|
||||
|
||||
int iconRes = LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
|
||||
item.isAddUser, item.isGuest, item.isAddSupervisedUser);
|
||||
return context.getDrawable(iconRes);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.user;
|
||||
import android.app.Activity;
|
||||
|
||||
import com.android.settingslib.users.EditUserInfoController;
|
||||
import com.android.systemui.user.data.repository.UserRepositoryModule;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
@@ -29,7 +30,11 @@ import dagger.multibindings.IntoMap;
|
||||
/**
|
||||
* Dagger module for User related classes.
|
||||
*/
|
||||
@Module
|
||||
@Module(
|
||||
includes = {
|
||||
UserRepositoryModule.class,
|
||||
}
|
||||
)
|
||||
public abstract class UserModule {
|
||||
|
||||
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.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -37,6 +38,7 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.constraintlayout.helper.widget.Flow
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.util.UserIcons
|
||||
import com.android.settingslib.Utils
|
||||
@@ -44,6 +46,8 @@ import com.android.systemui.Gefingerpoken
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
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.LOW_PENALTY
|
||||
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_ENABLED_ALPHA
|
||||
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 kotlin.math.ceil
|
||||
|
||||
@@ -63,11 +70,12 @@ private const val USER_VIEW = "user_view"
|
||||
class UserSwitcherActivity @Inject constructor(
|
||||
private val userSwitcherController: UserSwitcherController,
|
||||
private val broadcastDispatcher: BroadcastDispatcher,
|
||||
private val layoutInflater: LayoutInflater,
|
||||
private val falsingCollector: FalsingCollector,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val userManager: UserManager,
|
||||
private val userTracker: UserTracker
|
||||
private val userTracker: UserTracker,
|
||||
private val flags: FeatureFlags,
|
||||
private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
|
||||
) : ComponentActivity() {
|
||||
|
||||
private lateinit var parent: UserSwitcherRootView
|
||||
@@ -93,119 +101,31 @@ class UserSwitcherActivity @Inject constructor(
|
||||
false /* isAddSupervisedUser */
|
||||
)
|
||||
|
||||
private val adapter = object : 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()
|
||||
}
|
||||
}
|
||||
private val adapter: UserAdapter by lazy { UserAdapter() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.user_switcher_fullscreen)
|
||||
window.decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_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)
|
||||
|
||||
@@ -346,11 +266,18 @@ class UserSwitcherActivity @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (isUsingModernArchitecture()) {
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (isUsingModernArchitecture()) {
|
||||
return
|
||||
}
|
||||
|
||||
broadcastDispatcher.unregisterReceiver(broadcastReceiver)
|
||||
userTracker.removeCallback(userSwitchedCallback)
|
||||
@@ -376,6 +303,10 @@ class UserSwitcherActivity @Inject constructor(
|
||||
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(
|
||||
val parentContext: Context,
|
||||
val resource: Int,
|
||||
@@ -398,4 +329,114 @@ class UserSwitcherActivity @Inject constructor(
|
||||
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.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(
|
||||
/** Relevant user information. If `null`, this record is not a user but an option item. */
|
||||
@JvmField
|
||||
val info: UserInfo?,
|
||||
@JvmField val info: UserInfo? = null,
|
||||
/** An image representing the user. */
|
||||
@JvmField
|
||||
val picture: Bitmap?,
|
||||
@JvmField val picture: Bitmap? = null,
|
||||
/** Whether this record represents an option to switch to a guest user. */
|
||||
@JvmField
|
||||
val isGuest: Boolean,
|
||||
@JvmField val isGuest: Boolean = false,
|
||||
/** Whether this record represents the currently-selected user. */
|
||||
@JvmField
|
||||
val isCurrent: Boolean,
|
||||
@JvmField val isCurrent: Boolean = false,
|
||||
/** Whether this record represents an option to add another user to the device. */
|
||||
@JvmField
|
||||
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,
|
||||
) {
|
||||
@JvmField val isAddUser: Boolean = false,
|
||||
/**
|
||||
* 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 {
|
||||
return copy(isCurrent = isCurrent)
|
||||
}
|
||||
@@ -67,4 +58,11 @@ data class UserRecord(
|
||||
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 mOtherView: View
|
||||
@Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
|
||||
@Mock private lateinit var mUserInfo: UserInfo
|
||||
@Mock private lateinit var mLayoutInflater: LayoutInflater
|
||||
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
|
||||
private lateinit var adapter: UserDetailView.Adapter
|
||||
@@ -142,7 +141,7 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
|
||||
|
||||
private fun createUserRecord(current: Boolean, guest: Boolean) =
|
||||
UserRecord(
|
||||
mUserInfo,
|
||||
UserInfo(0 /* id */, "name", 0 /* flags */),
|
||||
mPicture,
|
||||
guest,
|
||||
current,
|
||||
|
||||
@@ -57,8 +57,6 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var inflatedUserDetailItemView: KeyguardUserDetailItemView
|
||||
@Mock
|
||||
private lateinit var userInfo: UserInfo
|
||||
@Mock
|
||||
private lateinit var layoutInflater: LayoutInflater
|
||||
@Mock
|
||||
private lateinit var keyguardUserSwitcherController: KeyguardUserSwitcherController
|
||||
@@ -188,7 +186,7 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
|
||||
|
||||
private fun createUserRecord(isCurrentUser: Boolean, isGuestUser: Boolean) =
|
||||
UserRecord(
|
||||
userInfo,
|
||||
UserInfo(0 /* id */, "name", 0 /* flags */),
|
||||
picture,
|
||||
isGuestUser,
|
||||
isCurrentUser,
|
||||
|
||||
@@ -24,9 +24,11 @@ import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.classifier.FalsingCollector
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.plugins.FalsingManager
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -54,6 +56,10 @@ class UserSwitcherActivityTest : SysuiTestCase() {
|
||||
private lateinit var userManager: UserManager
|
||||
@Mock
|
||||
private lateinit var userTracker: UserTracker
|
||||
@Mock
|
||||
private lateinit var flags: FeatureFlags
|
||||
@Mock
|
||||
private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -61,11 +67,12 @@ class UserSwitcherActivityTest : SysuiTestCase() {
|
||||
activity = UserSwitcherActivity(
|
||||
userSwitcherController,
|
||||
broadcastDispatcher,
|
||||
layoutInflater,
|
||||
falsingCollector,
|
||||
falsingManager,
|
||||
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