• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bedstead.nene.users;
18 
19 import static android.cts.testapisreflection.TestApisReflectionKt.setStopUserOnSwitch;
20 import static android.cts.testapisreflection.TestApisReflectionKt.getVisibleBackgroundUsersSupported;
21 import static android.cts.testapisreflection.TestApisReflectionKt.getVisibleBackgroundUsersOnDefaultDisplaySupported;
22 import static android.Manifest.permission.CREATE_USERS;
23 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
24 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
25 import static android.Manifest.permission.QUERY_USERS;
26 import static android.os.Build.VERSION.SDK_INT;
27 import static android.os.Build.VERSION_CODES.S;
28 import static android.os.Build.VERSION_CODES.S_V2;
29 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
30 import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM;
31 import static android.os.Process.myUserHandle;
32 
33 import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
34 import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
35 import static com.android.bedstead.nene.users.UserType.SYSTEM_USER_TYPE_NAME;
36 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_DEFAULT;
37 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_FALSE;
38 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_TRUE;
39 
40 import android.annotation.SuppressLint;
41 import android.app.ActivityManager;
42 import android.content.Context;
43 import android.cts.testapisreflection.TestApisReflectionKt;
44 import android.os.Build;
45 import android.os.UserHandle;
46 import android.os.UserManager;
47 import android.util.Log;
48 
49 import androidx.annotation.CheckResult;
50 import androidx.annotation.Nullable;
51 
52 import com.android.bedstead.nene.TestApis;
53 import com.android.bedstead.nene.annotations.Experimental;
54 import com.android.bedstead.nene.exceptions.AdbException;
55 import com.android.bedstead.nene.exceptions.AdbParseException;
56 import com.android.bedstead.nene.exceptions.NeneException;
57 import com.android.bedstead.nene.types.OptionalBoolean;
58 import com.android.bedstead.nene.utils.Poll;
59 import com.android.bedstead.nene.utils.ShellCommand;
60 import com.android.bedstead.nene.utils.Versions;
61 import com.android.bedstead.permissions.PermissionContext;
62 import com.android.bedstead.permissions.Permissions;
63 import com.google.errorprone.annotations.CanIgnoreReturnValue;
64 
65 import java.time.Duration;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Collection;
69 import java.util.Comparator;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.Iterator;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Set;
76 import java.util.concurrent.ConcurrentHashMap;
77 import java.util.function.Function;
78 import java.util.stream.Collectors;
79 import java.util.stream.Stream;
80 
81 public final class Users {
82 
83     private static final String LOG_TAG = "Users";
84 
85     static final int SYSTEM_USER_ID = 0;
86     private static final Duration WAIT_FOR_USER_TIMEOUT = Duration.ofMinutes(4);
87 
88     private Map<Integer, AdbUser> mCachedUsers = null;
89     private Map<String, UserType> mCachedUserTypes = null;
90     private Set<UserType> mCachedUserTypeValues = null;
91     private final AdbUserParser mParser;
92     private static final UserManager sUserManager =
93             TestApis.context().instrumentedContext().getSystemService(UserManager.class);
94     private Map<Integer, UserReference> mUsers = new ConcurrentHashMap<>();
95 
96     public static final Users sInstance = new Users();
97 
Users()98     private Users() {
99         mParser = AdbUserParser.get(SDK_INT);
100     }
101 
102     /** Get all {@link UserReference}s on the device. */
103     @CanIgnoreReturnValue
all()104     public Collection<UserReference> all() {
105         if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
106             fillCache();
107             return mCachedUsers.keySet().stream().map(UserReference::new)
108                     .collect(Collectors.toSet());
109         }
110 
111         return users().map(
112                 ui -> find(ui.getId())
113         ).collect(Collectors.toSet());
114     }
115 
116     /** Get all {@link UserReference}s in the instrumented user's profile group. */
117     @Experimental
profileGroup()118     public Collection<UserReference> profileGroup() {
119         return profileGroup(TestApis.users().instrumented());
120     }
121 
122     /** Get all {@link UserReference}s in the given profile group. */
123     @Experimental
profileGroup(UserReference user)124     public Collection<UserReference> profileGroup(UserReference user) {
125         return users().filter(ui -> ui.getProfileGroupId() == user.id())
126                 .map(ui -> find(ui.getId())).collect(Collectors.toSet());
127     }
128 
129     /**
130      * Gets a {@link UserReference} of the first human user on the device.
131      *
132      * @deprecated Use {@link #initial()} to ensure compatibility with Headless System User
133      * Mode devices.
134      */
135     @Deprecated
primary()136     public UserReference primary() {
137         return all()
138                 .stream()
139                 .filter(UserReference::isPrimary)
140                 .findFirst()
141                 .orElseThrow(IllegalStateException::new);
142     }
143 
144     /**
145      * Gets a {@link UserReference} of the first admin user on the device.
146      *
147      * @throws IllegalStateException when there's no admin
148      */
admin()149     public UserReference admin() {
150         return all()
151                 .stream()
152                 .sorted(Comparator.comparing(UserReference::id))
153                 .filter(UserReference::isAdmin)
154                 .findFirst()
155                 .orElseThrow(() -> new IllegalStateException("No admin user on device"));
156     }
157 
158     /**
159      * Gets a {@link UserReference} for the initial user for the device.
160      *
161      * <p>This will be the {@link #system()} user on most systems.</p>
162      */
initial()163     public UserReference initial() {
164         if (!isHeadlessSystemUserMode()) {
165             return system();
166         }
167         if (TestApis.packages().features().contains("android.hardware.type.automotive")) {
168             try {
169                 UserReference user =
170                         ShellCommand.builder("cmd car_service get-initial-user")
171                                 .executeAndParseOutput(i -> find(Integer.parseInt(i.trim())));
172 
173                 if (user.exists()) {
174                     return user;
175                 } else {
176                     Log.d(LOG_TAG, "Initial user " + user + " does not exist."
177                             + "Finding first non-system full user");
178                 }
179             } catch (AdbException e) {
180                 throw new NeneException("Error finding initial user on Auto", e);
181             }
182         }
183 
184         List<UserReference> users = new ArrayList<>(all());
185         users.sort(Comparator.comparingInt(UserReference::id));
186 
187         for (UserReference user : users) {
188             if (user.parent() != null) {
189                 continue;
190             }
191             if (user.id() == 0) {
192                 continue;
193             }
194 
195             return user;
196         }
197 
198         throw new NeneException("No initial user available");
199     }
200 
201     /** Get a {@link UserReference} for the user currently switched to. */
current()202     public UserReference current() {
203         if (Versions.meetsMinimumSdkVersionRequirement(S)) {
204             try (PermissionContext p =
205                          TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
206                 int currentUserId = ActivityManager.getCurrentUser();
207                 Log.d(LOG_TAG, "current(): finding " + currentUserId);
208                 return find(currentUserId);
209             }
210         }
211 
212         try {
213             return find((int) ShellCommand.builder("am get-current-user")
214                     .executeAndParseOutput(i -> Integer.parseInt(i.trim())));
215         } catch (AdbException e) {
216             throw new NeneException("Error getting current user", e);
217         }
218     }
219 
220     /** Get a {@link UserReference} for the user running the current test process. */
instrumented()221     public UserReference instrumented() {
222         return find(myUserHandle());
223     }
224 
225     /** Get a {@link UserReference} for the system user. */
system()226     public UserReference system() {
227         return find(0);
228     }
229 
230     /** Get a {@link UserReference} for the main user, if one exists. Null otherwise. */
231     @Nullable
232     @SuppressLint("NewApi")
main()233     public UserReference main() {
234         UserHandle mainUser;
235         try (PermissionContext p =
236                      TestApis.permissions().withPermission(QUERY_USERS)) {
237             mainUser = sUserManager.getMainUser();
238         }
239         if (mainUser == null) {
240             return null;
241         }
242         return find(mainUser);
243     }
244 
245     /** Get a {@link UserReference} by {@code id}. */
find(int id)246     public UserReference find(int id) {
247         if (!mUsers.containsKey(id)) {
248             mUsers.put(id, new UserReference(id));
249         }
250         return mUsers.get(id);
251     }
252 
253     /** Get a {@link UserReference} by {@code userHandle}. */
find(UserHandle userHandle)254     public UserReference find(UserHandle userHandle) {
255         return find(userHandle.getIdentifier());
256     }
257 
258     /** Get all supported {@link UserType}s. */
supportedTypes()259     public Set<UserType> supportedTypes() {
260         // TODO(b/203557600): Stop using adb
261         ensureSupportedTypesCacheFilled();
262         return mCachedUserTypeValues;
263     }
264 
265     /** Get a {@link UserType} with the given {@code typeName}, or {@code null} */
266     @Nullable
supportedType(String typeName)267     public UserType supportedType(String typeName) {
268         ensureSupportedTypesCacheFilled();
269         return mCachedUserTypes.get(typeName);
270     }
271 
272     /**
273      * Find all users which have the given {@link UserType}.
274      */
findUsersOfType(UserType userType)275     public Set<UserReference> findUsersOfType(UserType userType) {
276         if (userType == null) {
277             throw new NullPointerException();
278         }
279 
280         if (userType.baseType().contains(UserType.BaseType.PROFILE)) {
281             throw new NeneException("Cannot use findUsersOfType with profile type " + userType);
282         }
283 
284         return all().stream()
285                 .filter(u -> {
286                     try {
287                         return u.type().equals(userType);
288                     } catch (NeneException e) {
289                         return false;
290                     }
291                 })
292                 .collect(Collectors.toSet());
293     }
294 
295     /**
296      * Find a single user which has the given {@link UserType}.
297      *
298      * <p>If there are no users of the given type, {@code Null} will be returned.
299      *
300      * <p>If there is more than one user of the given type, {@link NeneException} will be thrown.
301      */
302     @Nullable
303     public UserReference findUserOfType(UserType userType) {
304         Set<UserReference> users = findUsersOfType(userType);
305 
306         if (users.isEmpty()) {
307             return null;
308         } else if (users.size() > 1) {
309             throw new NeneException("findUserOfType called but there is more than 1 user of type "
310                     + userType + ". Found: " + users);
311         }
312 
313         return users.iterator().next();
314     }
315 
316     /**
317      * Find all users which have the given {@link UserType} and the given parent.
318      */
319     public Set<UserReference> findProfilesOfType(UserType userType, UserReference parent) {
320         if (userType == null || parent == null) {
321             throw new NullPointerException();
322         }
323 
324         if (!userType.baseType().contains(UserType.BaseType.PROFILE)) {
325             throw new NeneException("Cannot use findProfilesOfType with non-profile type "
326                     + userType);
327         }
328 
329         return all().stream()
330                 .filter(u -> parent.equals(u.parent())
331                         && u.type().equals(userType))
332                 .collect(Collectors.toSet());
333     }
334 
335     /**
336      * Find all users which have the given {@link UserType} and the given parent.
337      *
338      * <p>If there are no users of the given type and parent, {@code Null} will be returned.
339      *
340      * <p>If there is more than one user of the given type and parent, {@link NeneException} will
341      * be thrown.
342      */
343     @Nullable
344     public UserReference findProfileOfType(UserType userType, UserReference parent) {
345         Set<UserReference> profiles = findProfilesOfType(userType, parent);
346 
347         if (profiles.isEmpty()) {
348             return null;
349         } else if (profiles.size() > 1) {
350             throw new NeneException("findProfileOfType called but there is more than 1 user of "
351                     + "type " + userType + " with parent " + parent + ". Found: " + profiles);
352         }
353 
354         return profiles.iterator().next();
355     }
356 
357 
358     /**
359      * Find all users which have the given {@link UserType} and the instrumented user as parent.
360      *
361      * <p>If there are no users of the given type and parent, {@code Null} will be returned.
362      *
363      * <p>If there is more than one user of the given type and parent, {@link NeneException} will
364      * be thrown.
365      */
366     @Nullable
367     public UserReference findProfileOfType(UserType userType) {
368         return findProfileOfType(userType, TestApis.users().instrumented());
369     }
370 
371     private void ensureSupportedTypesCacheFilled() {
372         if (mCachedUserTypes != null) {
373             // SupportedTypes don't change so don't need to be refreshed
374             return;
375         }
376         if (SDK_INT < Build.VERSION_CODES.R) {
377             mCachedUserTypes = new HashMap<>();
378             mCachedUserTypes.put(MANAGED_PROFILE_TYPE_NAME, managedProfileUserType());
379             mCachedUserTypes.put(SYSTEM_USER_TYPE_NAME, systemUserType());
380             mCachedUserTypes.put(SECONDARY_USER_TYPE_NAME, secondaryUserType());
381             mCachedUserTypeValues = new HashSet<>();
382             mCachedUserTypeValues.addAll(mCachedUserTypes.values());
383             return;
384         }
385 
386         fillCache();
387     }
388 
389     private UserType managedProfileUserType() {
390         UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
391         managedProfileMutableUserType.mName = MANAGED_PROFILE_TYPE_NAME;
392         managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.PROFILE));
393         managedProfileMutableUserType.mEnabled = true;
394         managedProfileMutableUserType.mMaxAllowed = -1;
395         managedProfileMutableUserType.mMaxAllowedPerParent = 1;
396         return new UserType(managedProfileMutableUserType);
397     }
398 
399     private UserType systemUserType() {
400         UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
401         managedProfileMutableUserType.mName = SYSTEM_USER_TYPE_NAME;
402         managedProfileMutableUserType.mBaseType =
403                 new HashSet<>(Arrays.asList(UserType.BaseType.FULL, UserType.BaseType.SYSTEM));
404         managedProfileMutableUserType.mEnabled = true;
405         managedProfileMutableUserType.mMaxAllowed = -1;
406         managedProfileMutableUserType.mMaxAllowedPerParent = -1;
407         return new UserType(managedProfileMutableUserType);
408     }
409 
410     private UserType secondaryUserType() {
411         UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
412         managedProfileMutableUserType.mName = SECONDARY_USER_TYPE_NAME;
413         managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.FULL));
414         managedProfileMutableUserType.mEnabled = true;
415         managedProfileMutableUserType.mMaxAllowed = -1;
416         managedProfileMutableUserType.mMaxAllowedPerParent = -1;
417         return new UserType(managedProfileMutableUserType);
418     }
419 
420     /**
421      * Create a new user.
422      */
423     @CheckResult
424     public UserBuilder createUser() {
425         return new UserBuilder();
426     }
427 
428     /**
429      * Get a {@link UserReference} to a user who does not exist.
430      */
431     public UserReference nonExisting() {
432         Set<Integer> userIds;
433         if (Versions.meetsMinimumSdkVersionRequirement(S)) {
434             userIds = users().map(ui -> ui.getId()).collect(Collectors.toSet());
435         } else {
436             fillCache();
437             userIds = mCachedUsers.keySet();
438         }
439 
440         int id = 0;
441 
442         while (userIds.contains(id)) {
443             id++;
444         }
445 
446         return find(id);
447     }
448 
449     private void fillCache() {
450         try {
451             // TODO: Replace use of adb on supported versions of Android
452             String userDumpsysOutput = ShellCommand.builder("dumpsys user").execute();
453             AdbUserParser.ParseResult result = mParser.parse(userDumpsysOutput);
454 
455             mCachedUsers = result.mUsers;
456             if (result.mUserTypes != null) {
457                 mCachedUserTypes = result.mUserTypes;
458             } else {
459                 ensureSupportedTypesCacheFilled();
460             }
461 
462             Iterator<Map.Entry<Integer, AdbUser>> iterator = mCachedUsers.entrySet().iterator();
463 
464             while (iterator.hasNext()) {
465                 Map.Entry<Integer, AdbUser> entry = iterator.next();
466 
467                 if (entry.getValue().isRemoving()) {
468                     // We don't expose users who are currently being removed
469                     iterator.remove();
470                     continue;
471                 }
472 
473                 AdbUser.MutableUser mutableUser = entry.getValue().mMutableUser;
474 
475                 if (SDK_INT < Build.VERSION_CODES.R) {
476                     if (entry.getValue().id() == SYSTEM_USER_ID) {
477                         mutableUser.mType = supportedType(SYSTEM_USER_TYPE_NAME);
478                         mutableUser.mIsPrimary = true;
479                     } else if (entry.getValue().hasFlag(AdbUser.FLAG_MANAGED_PROFILE)) {
480                         mutableUser.mType =
481                                 supportedType(MANAGED_PROFILE_TYPE_NAME);
482                         mutableUser.mIsPrimary = false;
483                     } else {
484                         mutableUser.mType =
485                                 supportedType(SECONDARY_USER_TYPE_NAME);
486                         mutableUser.mIsPrimary = false;
487                     }
488                 }
489 
490                 if (SDK_INT < S) {
491                     if (mutableUser.mType.baseType()
492                             .contains(UserType.BaseType.PROFILE)) {
493                         // We assume that all profiles before S were on the System User
494                         mutableUser.mParent = find(SYSTEM_USER_ID);
495                     }
496                 }
497             }
498 
499             mCachedUserTypeValues = new HashSet<>();
500             mCachedUserTypeValues.addAll(mCachedUserTypes.values());
501 
502         } catch (AdbException | AdbParseException e) {
503             throw new RuntimeException("Error filling cache", e);
504         }
505     }
506 
507     /**
508      * Block until the user with the given {@code userReference} to not exist or to be in the
509      * correct state.
510      *
511      * <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown.
512      */
513     @Nullable
514     UserReference waitForUserToNotExistOrMatch(
515             UserReference userReference, Function<UserReference, Boolean> userChecker) {
516         return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ false);
517     }
518 
519     @Nullable
520     private UserReference waitForUserToMatch(
521             UserReference userReference, Function<UserReference, Boolean> userChecker,
522             boolean waitForExist) {
523         // TODO(scottjonathan): This is pretty heavy because we resolve everything when we know we
524         //  are throwing away everything except one user. Optimise
525         try {
526             return Poll.forValue("user", () -> userReference)
527                     .toMeet((user) -> {
528                         if (user == null) {
529                             return !waitForExist;
530                         }
531                         return userChecker.apply(user);
532                     }).timeout(WAIT_FOR_USER_TIMEOUT)
533                     .errorOnFail("Expected user to meet requirement")
534                     .await();
535         } catch (AssertionError e) {
536             if (!userReference.exists()) {
537                 throw new NeneException(
538                         "Timed out waiting for user state for user "
539                                 + userReference + ". User does not exist.", e);
540             }
541             throw new NeneException(
542                     "Timed out waiting for user state, current state " + userReference, e
543             );
544         }
545     }
546 
547     /** Checks if private profile usertupe is supported on the device */
548     public boolean canAddPrivateProfile() {
549         if (Versions.meetsMinimumSdkVersionRequirement(VANILLA_ICE_CREAM)) {
550             try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
551                 return TestApisReflectionKt.canAddPrivateProfile(sUserManager);
552             }
553         }
554         return false;
555     }
556 
557     /** See {@link UserManager#isHeadlessSystemUserMode()}. */
558     @SuppressWarnings("NewApi")
559     public boolean isHeadlessSystemUserMode() {
560         if (Versions.meetsMinimumSdkVersionRequirement(S)) {
561             boolean value = UserManager.isHeadlessSystemUserMode();
562             Log.d(LOG_TAG, "isHeadlessSystemUserMode: " + value);
563             return value;
564         }
565 
566         Log.d(LOG_TAG, "isHeadlessSystemUserMode pre-S: false");
567         return false;
568     }
569 
570     /** See {@link UserManager#isVisibleBackgroundUsersSupported()}. */
571     @SuppressWarnings("NewApi")
572     public boolean isVisibleBackgroundUsersSupported() {
573         if (Versions.meetsMinimumSdkVersionRequirement(UPSIDE_DOWN_CAKE)) {
574             return getVisibleBackgroundUsersSupported(sUserManager);
575         }
576 
577         return false;
578     }
579 
580     /** See {@link UserManager#isVisibleBackgroundUsersOnDefaultDisplaySupported()}. */
581     @SuppressWarnings("NewApi")
582     public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported() {
583         if (Versions.meetsMinimumSdkVersionRequirement(UPSIDE_DOWN_CAKE)) {
584             return getVisibleBackgroundUsersOnDefaultDisplaySupported(sUserManager);
585         }
586 
587         return false;
588     }
589 
590     /**
591      * Set the stopBgUsersOnSwitch property.
592      *
593      * <p>This affects if background users will be swapped when switched away from on some devices.
594      */
595     public void setStopBgUsersOnSwitch(OptionalBoolean value) {
596         int intValue =
597                 (value == OptionalBoolean.TRUE)
598                         ? STOP_USER_ON_SWITCH_TRUE
599                         : (value == OptionalBoolean.FALSE)
600                                 ? STOP_USER_ON_SWITCH_FALSE
601                                 : STOP_USER_ON_SWITCH_DEFAULT;
602         if (!Versions.meetsMinimumSdkVersionRequirement(S_V2)) {
603             return;
604         }
605         Context context = TestApis.context().instrumentedContext();
606         try (PermissionContext p = TestApis.permissions()
607                 .withPermission(INTERACT_ACROSS_USERS)) {
608             setStopUserOnSwitch(context.getSystemService(ActivityManager.class), intValue);
609         }
610     }
611 
612     @Nullable
613     AdbUser fetchUser(int id) {
614         fillCache();
615         return mCachedUsers.get(id);
616     }
617 
618     @Experimental
619     public boolean supportsMultipleUsers() {
620         return UserManager.supportsMultipleUsers();
621     }
622 
623     /**
624      * Note: This method should not be run on < S.
625      */
626     static Stream<UserInfo> users() {
627         Versions.requireMinimumVersion(S);
628 
629         if (Permissions.sIgnorePermissions.get()) {
630             return getUsers();
631         }
632 
633         try (PermissionContext p =
634                      TestApis.permissions().withPermission(CREATE_USERS)
635                              .withPermissionOnVersionAtLeast(Versions.U, QUERY_USERS)) {
636             return getUsers();
637         }
638     }
639 
640     private static Stream<UserInfo> getUsers() {
641         return TestApisReflectionKt.getUsers(sUserManager,
642                 /* excludePartial= */ false,
643                 /* excludeDying= */ true,
644                 /* excludePreCreated= */ false).stream()
645                 .map(ui -> new UserInfo(ui));
646     }
647 
648     /**
649      * Gets the maximum number of users supported by the device
650      */
651     public int getMaxNumberOfUsersSupported() {
652         try {
653             return ShellCommand.builder("pm get-max-users")
654                     .validate((output) -> output.startsWith("Maximum supported users:"))
655                     .executeAndParseOutput((output) ->
656                             Integer.parseInt(output.split(": ", 2)[1].trim())
657                     );
658         } catch (AdbException e) {
659             throw new IllegalStateException("Invalid command output", e);
660         }
661     }
662 }
663