diff --git a/packages/SystemUI/res/drawable/ic_add_supervised_user.xml b/packages/SystemUI/res/drawable/ic_add_supervised_user.xml new file mode 100644 index 0000000000000..627743ed16697 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_add_supervised_user.xml @@ -0,0 +1,19 @@ + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d39e295689864..3e9e00129bf72 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2402,4 +2402,6 @@ Add + + @*android:string/supervised_user_creation_label diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 3ece240bc5766..f1093d18a28b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -46,6 +46,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.telephony.TelephonyCallback; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -159,6 +160,7 @@ public class UserSwitcherController implements Dumpable { private final AtomicBoolean mGuestCreationScheduled; private FalsingManager mFalsingManager; private View mView; + private String mCreateSupervisedUserPackage; @Inject public UserSwitcherController(Context context, @@ -255,6 +257,9 @@ public class UserSwitcherController implements Dumpable { keyguardStateController.addCallback(mCallback); listenForCallState(); + mCreateSupervisedUserPackage = mContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage); + dumpManager.registerDumpable(getClass().getSimpleName(), this); refreshUsers(UserHandle.USER_NULL); @@ -307,14 +312,10 @@ public class UserSwitcherController implements Dumpable { // User 0 boolean canSwitchUsers = mUserManager.getUserSwitchability( UserHandle.of(mUserTracker.getUserId())) == SWITCHABILITY_STATUS_OK; - UserInfo currentUserInfo = null; UserRecord guestRecord = null; for (UserInfo info : infos) { boolean isCurrent = currentId == info.id; - if (isCurrent) { - currentUserInfo = info; - } boolean switchToEnabled = canSwitchUsers || isCurrent; if (info.isEnabled()) { if (info.isGuest()) { @@ -322,7 +323,8 @@ public class UserSwitcherController implements Dumpable { // the icon shouldn't be enabled even if the user is current guestRecord = new UserRecord(info, null /* picture */, true /* isGuest */, isCurrent, false /* isAddUser */, - false /* isRestricted */, canSwitchUsers); + false /* isRestricted */, canSwitchUsers, + false /* isAddSupervisedUser */); } else if (info.supportsSwitchToByUser()) { Bitmap picture = bitmaps.get(info.id); if (picture == null) { @@ -337,7 +339,7 @@ public class UserSwitcherController implements Dumpable { } records.add(new UserRecord(info, picture, false /* isGuest */, isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled)); + switchToEnabled, false /* isAddSupervisedUser */)); } } } @@ -345,19 +347,6 @@ public class UserSwitcherController implements Dumpable { Prefs.putBoolean(mContext, Key.SEEN_MULTI_USER, true); } - boolean systemCanCreateUsers = !mUserManager.hasBaseUserRestriction( - UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM); - boolean currentUserCanCreateUsers = currentUserInfo != null - && (currentUserInfo.isAdmin() - || currentUserInfo.id == UserHandle.USER_SYSTEM) - && systemCanCreateUsers; - boolean anyoneCanCreateUsers = systemCanCreateUsers && addUsersWhenLocked; - boolean canCreateGuest = (currentUserCanCreateUsers || anyoneCanCreateUsers) - && guestRecord == null; - boolean canCreateUser = (currentUserCanCreateUsers || anyoneCanCreateUsers) - && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY); - boolean createIsRestricted = !addUsersWhenLocked; - if (guestRecord == null) { if (mGuestUserAutoCreated) { // If mGuestIsResetting=true, the switch should be disabled since @@ -368,13 +357,14 @@ public class UserSwitcherController implements Dumpable { guestRecord = new UserRecord(null /* info */, null /* picture */, true /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, - isSwitchToGuestEnabled); + isSwitchToGuestEnabled, false /* isAddSupervisedUser */); checkIfAddUserDisallowedByAdminOnly(guestRecord); records.add(guestRecord); - } else if (canCreateGuest) { + } else if (canCreateGuest(guestRecord != null)) { guestRecord = new UserRecord(null /* info */, null /* picture */, true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, createIsRestricted, canSwitchUsers); + false /* isAddUser */, createIsRestricted(), canSwitchUsers, + false /* isAddSupervisedUser */); checkIfAddUserDisallowedByAdminOnly(guestRecord); records.add(guestRecord); } @@ -382,10 +372,19 @@ public class UserSwitcherController implements Dumpable { records.add(guestRecord); } - if (canCreateUser) { + if (canCreateUser()) { UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, false /* isGuest */, false /* isCurrent */, true /* isAddUser */, - createIsRestricted, canSwitchUsers); + createIsRestricted(), canSwitchUsers, + false /* isAddSupervisedUser */); + checkIfAddUserDisallowedByAdminOnly(addUserRecord); + records.add(addUserRecord); + } + + if (canCreateSupervisedUser()) { + UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, + false /* isGuest */, false /* isCurrent */, false /* isAddUser */, + createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); checkIfAddUserDisallowedByAdminOnly(addUserRecord); records.add(addUserRecord); } @@ -403,6 +402,40 @@ public class UserSwitcherController implements Dumpable { }.execute((SparseArray) bitmaps); } + boolean systemCanCreateUsers() { + return !mUserManager.hasBaseUserRestriction( + UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM); + } + + boolean currentUserCanCreateUsers() { + UserInfo currentUser = mUserTracker.getUserInfo(); + return currentUser != null + && (currentUser.isAdmin() || mUserTracker.getUserId() == UserHandle.USER_SYSTEM) + && systemCanCreateUsers(); + } + + boolean anyoneCanCreateUsers() { + return systemCanCreateUsers() && mAddUsersFromLockScreen; + } + + boolean canCreateGuest(boolean hasExistingGuest) { + return (currentUserCanCreateUsers() || anyoneCanCreateUsers()) + && !hasExistingGuest; + } + + boolean canCreateUser() { + return (currentUserCanCreateUsers() || anyoneCanCreateUsers()) + && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY); + } + + boolean createIsRestricted() { + return mAddUsersFromLockScreen; + } + + boolean canCreateSupervisedUser() { + return !TextUtils.isEmpty(mCreateSupervisedUserPackage) && canCreateUser(); + } + private void pauseRefreshUsers() { if (!mPauseRefreshUsers) { mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS); @@ -485,6 +518,9 @@ public class UserSwitcherController implements Dumpable { } else if (record.isAddUser) { showAddUserDialog(dialogShower); return; + } else if (record.isAddSupervisedUser) { + startSupervisedUserActivity(); + return; } else { id = record.info.id; } @@ -561,6 +597,22 @@ public class UserSwitcherController implements Dumpable { } } + private void startSupervisedUserActivity() { + final Intent intent = new Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(mCreateSupervisedUserPackage) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // TODO(b/209659998): [to-be-removed] fallback activity for supervised user creation. + if (mContext.getPackageManager().resolveActivity(intent, 0) == null) { + intent.setPackage(null) + .setClassName("com.android.settings", + "com.android.settings.users.AddSupervisedUserActivity"); + } + + mContext.startActivity(intent); + } + private void listenForCallState() { mTelephonyListenerManager.addCallStateListener(mPhoneStateListener); } @@ -941,6 +993,8 @@ public class UserSwitcherController implements Dumpable { } } else if (item.isAddUser) { return context.getString(R.string.user_add_user); + } else if (item.isAddSupervisedUser) { + return context.getString(R.string.add_user_supervised); } else { return item.info.name; } @@ -958,6 +1012,8 @@ public class UserSwitcherController implements Dumpable { iconRes = R.drawable.ic_add_circle; } else if (item.isGuest) { iconRes = R.drawable.ic_avatar_guest_user; + } else if (item.isAddSupervisedUser) { + iconRes = R.drawable.ic_add_supervised_user; } else { iconRes = R.drawable.ic_avatar_user; } @@ -1000,6 +1056,7 @@ public class UserSwitcherController implements Dumpable { public final boolean isGuest; public final boolean isCurrent; public final boolean isAddUser; + public final boolean isAddSupervisedUser; /** If true, the record is only visible to the owner and only when unlocked. */ public final boolean isRestricted; public boolean isDisabledByAdmin; @@ -1007,7 +1064,8 @@ public class UserSwitcherController implements Dumpable { public boolean isSwitchToEnabled; public UserRecord(UserInfo info, Bitmap picture, boolean isGuest, boolean isCurrent, - boolean isAddUser, boolean isRestricted, boolean isSwitchToEnabled) { + boolean isAddUser, boolean isRestricted, boolean isSwitchToEnabled, + boolean isAddSupervisedUser) { this.info = info; this.picture = picture; this.isGuest = isGuest; @@ -1015,11 +1073,12 @@ public class UserSwitcherController implements Dumpable { this.isAddUser = isAddUser; this.isRestricted = isRestricted; this.isSwitchToEnabled = isSwitchToEnabled; + this.isAddSupervisedUser = isAddSupervisedUser; } public UserRecord copyWithIsCurrent(boolean _isCurrent) { return new UserRecord(info, picture, isGuest, _isCurrent, isAddUser, isRestricted, - isSwitchToEnabled); + isSwitchToEnabled, isAddSupervisedUser); } public int resolveId() { @@ -1043,6 +1102,7 @@ public class UserSwitcherController implements Dumpable { } if (isGuest) sb.append(" "); if (isAddUser) sb.append(" "); + if (isAddSupervisedUser) sb.append(" "); if (isCurrent) sb.append(" "); if (picture != null) sb.append(" "); if (isRestricted) sb.append(" "); diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt index d6a8ab270b845..02e71897500f5 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -71,8 +71,7 @@ class UserSwitcherActivity @Inject constructor( private lateinit var broadcastReceiver: BroadcastReceiver private var popupMenu: UserSwitcherPopupMenu? = null private lateinit var addButton: View - private var addUserItem: UserRecord? = null - private var addGuestItem: UserRecord? = null + private var addUserRecords = mutableListOf() private val adapter = object : BaseUserAdapter(userSwitcherController) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { @@ -203,8 +202,7 @@ class UserSwitcherActivity @Inject constructor( private fun showPopupMenu() { val items = mutableListOf() - addUserItem?.let { items.add(it) } - addGuestItem?.let { items.add(it) } + addUserRecords.forEach { items.add(it) } var popupMenuAdapter = ItemAdapter( this, @@ -249,10 +247,10 @@ class UserSwitcherActivity @Inject constructor( val flow = requireViewById(R.id.flow) for (i in 0 until adapter.getCount()) { val item = adapter.getItem(i) - if (item.isAddUser) { - addUserItem = item - } else if (item.isGuest && item.info == null) { - addGuestItem = item + if (item.isAddUser || + item.isAddSupervisedUser || + item.isGuest && item.info == null) { + addUserRecords.add(item) } else { val userView = adapter.getView(i, null, parent) userView.setId(View.generateViewId()) @@ -273,7 +271,7 @@ class UserSwitcherActivity @Inject constructor( } } - if (addUserItem != null || addGuestItem != null) { + if (!addUserRecords.isEmpty()) { addButton.visibility = View.VISIBLE } else { addButton.visibility = View.GONE diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 24b01e079b428..6736bfd217404 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -307,7 +307,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { UserInfo info = new UserInfo(i /* id */, "Name: " + i, null /* iconPath */, 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, - false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */)); + false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */)); } return users; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index 3a3d1546984d1..9b0142d6a8fe4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -147,5 +147,6 @@ class UserDetailViewAdapterTest : SysuiTestCase() { current, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt index e479882ac50a8..0dd6cbb7995ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt @@ -193,5 +193,6 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() { isCurrentUser, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt index 9a7e702152b49..9dabf6907f7cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.util.time.FakeSystemClock import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -130,6 +131,21 @@ class UserSwitcherControllerTest : SysuiTestCase() { .thenReturn(true) `when`(notificationShadeWindowView.context).thenReturn(context) + // Since userSwitcherController involves InteractionJankMonitor. + // Let's fulfill the dependencies. + val mockedContext = mock(Context::class.java) + doReturn(mockedContext).`when`(notificationShadeWindowView).context + doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow + doNothing().`when`(threadedRenderer).addObserver(any()) + doNothing().`when`(threadedRenderer).removeObserver(any()) + doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer + + picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) + + setupController() + } + + private fun setupController() { userSwitcherController = UserSwitcherController( context, activityManager, @@ -153,18 +169,6 @@ class UserSwitcherControllerTest : SysuiTestCase() { dumpManager, dialogLaunchAnimator) userSwitcherController.mPauseRefreshUsers = true - - // Since userSwitcherController involves InteractionJankMonitor. - // Let's fulfill the dependencies. - val mockedContext = mock(Context::class.java) - doReturn(mockedContext).`when`(notificationShadeWindowView).context - doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow - doNothing().`when`(threadedRenderer).addObserver(any()) - doNothing().`when`(threadedRenderer).removeObserver(any()) - doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer - userSwitcherController.init(notificationShadeWindowView) - - picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) userSwitcherController.init(notificationShadeWindowView) } @@ -177,7 +181,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { false /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(ownerId) `when`(userTracker.userInfo).thenReturn(ownerInfo) @@ -196,7 +201,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { false /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(ownerId) `when`(userTracker.userInfo).thenReturn(ownerInfo) @@ -220,7 +226,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { false /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(ownerId) `when`(userTracker.userInfo).thenReturn(ownerInfo) @@ -240,7 +247,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { true /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestInfo.id) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -262,7 +270,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { true /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestInfo.id) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -283,7 +292,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { true /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestInfo.id) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -302,7 +312,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { true /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestId) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -323,7 +334,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { false /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestId) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -357,7 +369,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { false /* current */, false /* isAddUser */, false /* isRestricted */, - true /* isSwitchToEnabled */) + true /* isSwitchToEnabled */, + false /* isAddSupervisedUser */) `when`(userTracker.userId).thenReturn(guestId) `when`(userTracker.userInfo).thenReturn(guestInfo) @@ -389,7 +402,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { userSwitcherController.users.add(UserSwitcherController.UserRecord( UserInfo(id, name, 0), null, false, isCurrent, false, - false, false + false, false, false )) } val bgUserName = "background_user" @@ -412,4 +425,42 @@ class UserSwitcherControllerTest : SysuiTestCase() { `when`(userTracker.userId).thenReturn(1) assertEquals(false, userSwitcherController.isSystemUser) } + + @Test + fun testCanCreateSupervisedUserWithConfiguredPackage() { + // GIVEN the supervised user creation package is configured + `when`(context.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage)) + .thenReturn("some_pkg") + + // AND the current user is allowed to create new users + `when`(userTracker.userId).thenReturn(ownerId) + `when`(userTracker.userInfo).thenReturn(ownerInfo) + + // WHEN the controller is started with the above config + setupController() + testableLooper.processAllMessages() + + // THEN a supervised user can be constructed + assertTrue(userSwitcherController.canCreateSupervisedUser()) + } + + @Test + fun testCannotCreateSupervisedUserWithConfiguredPackage() { + // GIVEN the supervised user creation package is NOT configured + `when`(context.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage)) + .thenReturn(null) + + // AND the current user is allowed to create new users + `when`(userTracker.userId).thenReturn(ownerId) + `when`(userTracker.userInfo).thenReturn(ownerInfo) + + // WHEN the controller is started with the above config + setupController() + testableLooper.processAllMessages() + + // THEN a supervised user can NOT be constructed + assertFalse(userSwitcherController.canCreateSupervisedUser()) + } }