Add supervised user support

Supports adding a supervised user when configured. Modify the
UserSwitcherController so that all supports dialogs will get the new
"Add user" option if available.

Fixes: 218310361
Test: atest UserSwitcherControllerTest KeyguardUserSwitcherAdapterTest UserDetailViewAdapterTest KeyguardSecurityContainerTest
Change-Id: Ib79dcde2c726cc9508aef667b43662152d047137
This commit is contained in:
Matt Pietal
2022-02-08 11:10:28 -05:00
parent 4ff2a4e611
commit 2f5d6ec11e
8 changed files with 193 additions and 60 deletions

View File

@@ -0,0 +1,19 @@
<!--
~ 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.
-->
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:drawable="@*android:drawable/ic_add_supervised_user" />
</layer-list>

View File

@@ -2402,4 +2402,6 @@
<!-- Generic "add" string [CHAR LIMIT=NONE] -->
<string name="add">Add</string>
<!-- Add supervised user -->
<string name="add_user_supervised" translatable="false">@*android:string/supervised_user_creation_label</string>
</resources>

View File

@@ -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(" <isGuest>");
if (isAddUser) sb.append(" <isAddUser>");
if (isAddSupervisedUser) sb.append(" <isAddSupervisedUser>");
if (isCurrent) sb.append(" <isCurrent>");
if (picture != null) sb.append(" <hasPicture>");
if (isRestricted) sb.append(" <isRestricted>");

View File

@@ -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<UserRecord>()
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<UserRecord>()
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<Flow>(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

View File

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

View File

@@ -147,5 +147,6 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
current,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */)
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */)
}

View File

@@ -193,5 +193,6 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
isCurrentUser,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */)
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */)
}

View File

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