• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.car.cts;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import static org.junit.Assert.fail;
22 import static org.junit.Assume.assumeTrue;
23 
24 import android.service.pm.PackageProto;
25 import android.service.pm.PackageProto.UserPermissionsProto;
26 import android.service.pm.PackageServiceDumpProto;
27 
28 import com.android.compatibility.common.util.CommonTestUtils;
29 import com.android.compatibility.common.util.CommonTestUtils.BooleanSupplierWithThrow;
30 import com.android.tradefed.device.CollectingByteOutputReceiver;
31 import com.android.tradefed.device.DeviceNotAvailableException;
32 import com.android.tradefed.device.ITestDevice;
33 import com.android.tradefed.log.LogUtil.CLog;
34 import com.android.tradefed.testtype.ITestInformationReceiver;
35 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
36 
37 import org.junit.After;
38 import org.junit.AssumptionViolatedException;
39 import org.junit.Before;
40 import org.junit.Rule;
41 import org.junit.rules.TestRule;
42 import org.junit.runner.Description;
43 import org.junit.runners.model.Statement;
44 
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.function.Function;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
53 import java.util.stream.Collectors;
54 
55 /**
56  * Base class for all test cases.
57  */
58 // NOTE: must be public because of @Rules
59 public abstract class CarHostJUnit4TestCase extends BaseHostJUnit4Test {
60 
61     private static final int SYSTEM_USER_ID = 0;
62 
63     private static final int DEFAULT_TIMEOUT_SEC = 20;
64     protected static final int SYSTEM_RESTART_TIMEOUT_SEC = 120;
65 
66     private static final Pattern CREATE_USER_OUTPUT_PATTERN = Pattern.compile("id=(\\d+)");
67 
68     private static final String USER_PREFIX = "CtsCarHostTestCases";
69 
70     /**
71      * User pattern in the output of "cmd user list --all -v"
72      * TEXT id=<id> TEXT name=<name>, TEX flags=<flags> TEXT
73      * group 1: id group 2: name group 3: flags group 4: other state(like "(running)")
74      */
75     private static final Pattern USER_PATTERN = Pattern.compile(
76             ".*id=(\\d+).*name=([^\\s,]+).*flags=(\\S+)(.*)");
77 
78     private static final int USER_PATTERN_GROUP_ID = 1;
79     private static final int USER_PATTERN_GROUP_NAME = 2;
80     private static final int USER_PATTERN_GROUP_FLAGS = 3;
81     private static final int USER_PATTERN_GROUP_OTHER_STATE = 4;
82 
83     /**
84      * User's package permission pattern string format in the output of "dumpsys package PKG_NAME"
85      */
86     protected static final String APP_APK = "CtsCarApp.apk";
87     protected static final String APP_PKG = "android.car.cts.app";
88 
89     @Rule
90     public final RequiredFeatureRule mHasAutomotiveRule = new RequiredFeatureRule(this,
91             "android.hardware.type.automotive");
92 
93     private final HashSet<Integer> mUsersToBeRemoved = new HashSet<>();
94 
95     private int mInitialUserId;
96     private Integer mInitialMaximumNumberOfUsers;
97 
98     // It is possible that during test initial user is deleted and it is not possible to switch
99     // to the initial User. This boolean controls if test should switch to initial user on clean up.
100     private boolean mSwitchToInitialUser = true;
101 
102     /**
103      * Saves multi-user state so it can be restored after the test.
104      */
105     @Before
saveUserState()106     public void saveUserState() throws Exception {
107         removeUsers(USER_PREFIX);
108 
109         mInitialUserId = getCurrentUserId();
110     }
111 
112     /**
113      * Restores multi-user state from before the test.
114      */
115     @After
restoreUsersState()116     public void restoreUsersState() throws Exception {
117         int currentUserId = getCurrentUserId();
118         CLog.d("restoreUsersState(): initial user: %d, current user: %d, created users: %s "
119                 + "max number of users: %d",
120                 mInitialUserId, currentUserId, mUsersToBeRemoved, mInitialMaximumNumberOfUsers);
121         if (currentUserId != mInitialUserId && mSwitchToInitialUser) {
122             CLog.i("Switching back from %d to %d", currentUserId, mInitialUserId);
123             switchUser(mInitialUserId);
124         }
125 
126         if (!mUsersToBeRemoved.isEmpty()) {
127             CLog.i("Removing users %s", mUsersToBeRemoved);
128             for (int userId : mUsersToBeRemoved) {
129                 removeUser(userId);
130             }
131         }
132 
133         // Should have been removed above, but as the saying goes, better safe than sorry...
134         removeUsers(USER_PREFIX);
135 
136         if (mInitialMaximumNumberOfUsers != null) {
137             CLog.i("Restoring max number of users to %d", mInitialMaximumNumberOfUsers);
138             setMaxNumberUsers(mInitialMaximumNumberOfUsers);
139         }
140     }
141 
142     /**
143      * It is possible that during test initial user is deleted and it is not possible to switch to
144      * the initial User. This method controls if test should switch to initial user on clean up.
145      */
doNotSwitchToInitialUserAfterTest()146     public void doNotSwitchToInitialUserAfterTest() {
147         mSwitchToInitialUser = false;
148     }
149 
150     /**
151      * Returns whether device is in headless system user mode.
152      */
isHeadlessSystemUserMode()153     boolean isHeadlessSystemUserMode() throws Exception {
154         String result = getDevice()
155                 .executeShellCommand("getprop ro.fw.mu.headless_system_user").trim();
156         return Boolean.valueOf(result);
157     }
158 
159     /**
160      * Makes sure the device supports multiple users, throwing {@link AssumptionViolatedException}
161      * if it doesn't.
162      */
assumeSupportsMultipleUsers()163     protected final void assumeSupportsMultipleUsers() throws Exception {
164         assumeTrue("device does not support multi-user",
165                 getDevice().getMaxNumberOfUsersSupported() > 1);
166     }
167 
168     /**
169      * Makes sure the device can add {@code numberOfUsers} new users, increasing limit if needed or
170      * failing if not possible.
171      */
requiresExtraUsers(int numberOfUsers)172     protected final void requiresExtraUsers(int numberOfUsers) throws Exception {
173         assumeSupportsMultipleUsers();
174 
175         int maxNumber = getDevice().getMaxNumberOfUsersSupported();
176         int currentNumber = getDevice().listUsers().size();
177 
178         if (currentNumber + numberOfUsers <= maxNumber) return;
179 
180         if (!getDevice().isAdbRoot()) {
181             failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ false);
182         }
183 
184         // Increase limit...
185         mInitialMaximumNumberOfUsers = maxNumber;
186         setMaxNumberUsers(maxNumber + numberOfUsers);
187 
188         // ...and try again
189         maxNumber = getDevice().getMaxNumberOfUsersSupported();
190         if (currentNumber + numberOfUsers > maxNumber) {
191             failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ true);
192         }
193     }
194 
failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber, boolean isAdbRoot)195     private void failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber,
196             boolean isAdbRoot) {
197         String reason = isAdbRoot ? "failed to increase it"
198                 : "cannot be increased without adb root";
199         String existingUsers = "";
200         try {
201             existingUsers = "Existing users: " + executeCommand("cmd user list --all -v");
202         } catch (Exception e) {
203             // ignore
204         }
205         fail("Cannot create " + numberOfUsers + " users: current number is " + currentNumber
206                 + ", limit is " + maxNumber + " and could not be increased (" + reason + "). "
207                 + existingUsers);
208     }
209 
210     /**
211      * Executes the shell command and returns the output.
212      */
executeCommand(String command, Object... args)213     protected String executeCommand(String command, Object... args) throws Exception {
214         String fullCommand = String.format(command, args);
215         return getDevice().executeShellCommand(fullCommand);
216     }
217 
218     /**
219      * Executes the shell command and parses output with {@code resultParser}.
220      */
executeAndParseCommand(Function<String, T> resultParser, String command, Object... args)221     protected <T> T executeAndParseCommand(Function<String, T> resultParser,
222             String command, Object... args) throws Exception {
223         String output = executeCommand(command, args);
224         return resultParser.apply(output);
225     }
226 
227     /**
228      * Executes the shell command and parses the Matcher output with {@code resultParser}, failing
229      * with {@code matchNotFoundErrorMessage} if it didn't match the {@code regex}.
230      */
executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage, Function<Matcher, T> resultParser, String command, Object... args)231     protected <T> T executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage,
232             Function<Matcher, T> resultParser,
233             String command, Object... args) throws Exception {
234         String output = executeCommand(command, args);
235         Matcher matcher = regex.matcher(output);
236         if (!matcher.find()) {
237             fail(matchNotFoundErrorMessage + ". Shell command: '" + String.format(command, args)
238                     + "'. Output: " + output.trim() + ". Regex: " + regex);
239         }
240         return resultParser.apply(matcher);
241     }
242 
243     /**
244      * Executes the shell command and parses the Matcher output with {@code resultParser}.
245      */
executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser, String command, Object... args)246     protected <T> T executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser,
247             String command, Object... args) throws Exception {
248         String output = executeCommand(command, args);
249         return resultParser.apply(regex.matcher(output));
250     }
251 
252     /**
253      * Executes the shell command that returns all users (including pre-created and partial)
254      * and returns {@code function} applied to them.
255      */
onAllUsers(Function<List<UserInfo>, T> function)256     public <T> T onAllUsers(Function<List<UserInfo>, T> function) throws Exception {
257         ArrayList<UserInfo> allUsers = executeAndParseCommand(USER_PATTERN, (matcher) -> {
258             ArrayList<UserInfo> users = new ArrayList<>();
259             while (matcher.find()) {
260                 users.add(new UserInfo(matcher));
261             }
262             return users;
263         }, "cmd user list --all -v");
264         return function.apply(allUsers);
265     }
266 
267     /**
268      * Gets the info for the given user.
269      */
getUserInfo(int userId)270     public UserInfo getUserInfo(int userId) throws Exception {
271         return onAllUsers((allUsers) -> allUsers.stream()
272                 .filter((u) -> u.id == userId))
273                         .findFirst().get();
274     }
275 
276     /**
277      * Gets all persistent (i.e., non-ephemeral) users.
278      */
getAllPersistentUsers()279     protected List<Integer> getAllPersistentUsers() throws Exception {
280         return onAllUsers((allUsers) -> allUsers.stream()
281                 .filter((u) -> !u.flags.contains("DISABLED") && !u.flags.contains("EPHEMERAL")
282                         && !u.otherState.contains("pre-created")
283                         && !u.otherState.contains("partial"))
284                 .map((u) -> u.id).collect(Collectors.toList()));
285     }
286 
287     /**
288      * Sets the maximum number of users that can be created for this car.
289      *
290      * @throws IllegalStateException if adb is not running as root
291      */
setMaxNumberUsers(int numUsers)292     protected void setMaxNumberUsers(int numUsers) throws Exception {
293         if (!getDevice().isAdbRoot()) {
294             throw new IllegalStateException("must be running adb root");
295         }
296         executeCommand("setprop fw.max_users %d", numUsers);
297     }
298 
299     /**
300      * Gets the current user's id.
301      */
getCurrentUserId()302     protected int getCurrentUserId() throws DeviceNotAvailableException {
303         return getDevice().getCurrentUser();
304     }
305 
306     /**
307      * Creates a full user with car service shell command.
308      */
createFullUser(String name)309     protected int createFullUser(String name) throws Exception {
310         return createUser(name, /* flags= */ 0, /* isGuest= */ false);
311     }
312 
313     /**
314      * Creates a full guest with car service shell command.
315      */
createGuestUser(String name)316     protected int createGuestUser(String name) throws Exception {
317         return createUser(name, /* flags= */ 0, /* isGuest= */ true);
318     }
319 
320     /**
321      * Creates a flexible user with car service shell command.
322      *
323      * <p><b>NOTE: </b>it uses User HAL flags, not core Android's.
324      */
createUser(String name, int flags, boolean isGuest)325     protected int createUser(String name, int flags, boolean isGuest) throws Exception {
326         name = USER_PREFIX + "." + name;
327         waitForCarServiceReady();
328         int userId = executeAndParseCommand(CREATE_USER_OUTPUT_PATTERN,
329                 "Could not create user with name " + name
330                         + ", flags " + flags + ", guest " + isGuest,
331                 matcher -> Integer.parseInt(matcher.group(1)),
332                 "cmd car_service create-user --flags %d %s%s",
333                 flags, (isGuest ? "--guest " : ""), name);
334         markUserForRemovalAfterTest(userId);
335         return userId;
336     }
337 
338     /**
339      * Marks a user to be removed at the end of the tests.
340      */
markUserForRemovalAfterTest(int userId)341     protected void markUserForRemovalAfterTest(int userId) {
342         mUsersToBeRemoved.add(userId);
343     }
344 
345     /**
346      * Waits until the given user is initialized.
347      */
waitForUserInitialized(int userId)348     protected void waitForUserInitialized(int userId) throws Exception {
349         CommonTestUtils.waitUntil("timed out waiting for user " + userId + " initialization",
350                 DEFAULT_TIMEOUT_SEC, () -> isUserInitialized(userId));
351     }
352 
353     /**
354      * Waits until the system server is ready.
355      */
waitForCarServiceReady()356     protected void waitForCarServiceReady() throws Exception {
357         CommonTestUtils.waitUntil("timed out waiting for car service",
358                 DEFAULT_TIMEOUT_SEC, () -> isCarServiceReady());
359     }
360 
isCarServiceReady()361     protected boolean isCarServiceReady() {
362         String cmd = "service check car_service";
363         try {
364             String output = getDevice().executeShellCommand(cmd);
365             return !output.endsWith("not found");
366         } catch (Exception e) {
367             CLog.i("%s failed: %s", cmd, e.getMessage());
368         }
369         return false;
370     }
371 
372     /**
373      * Asserts that the given user is initialized.
374      */
assertUserInitialized(int userId)375     protected void assertUserInitialized(int userId) throws Exception {
376         assertWithMessage("User %s not initialized", userId).that(isUserInitialized(userId))
377                 .isTrue();
378         CLog.v("User %d is initialized", userId);
379     }
380 
381     /**
382      * Checks if the given user is initialized.
383      */
isUserInitialized(int userId)384     protected boolean isUserInitialized(int userId) throws Exception {
385         UserInfo userInfo = getUserInfo(userId);
386         CLog.v("isUserInitialized(%d): %s", userId, userInfo);
387         return userInfo.flags.contains("INITIALIZED");
388     }
389 
390     /**
391      * Checks if the given user is ephemeral.
392      */
isUserEphemeral(int userId)393     protected boolean isUserEphemeral(int userId) throws Exception {
394         UserInfo userInfo = getUserInfo(userId);
395         CLog.v("isUserEphemeral(%d): %s", userId, userInfo);
396         return userInfo.flags.contains("EPHEMERAL");
397     }
398 
399     /**
400      * Switches the current user.
401      */
switchUser(int userId)402     protected void switchUser(int userId) throws Exception {
403         waitForCarServiceReady();
404         String output = executeCommand("cmd car_service switch-user %d", userId);
405         if (!output.contains("STATUS_SUCCESSFUL")) {
406             throw new IllegalStateException("Failed to switch to user " + userId + ": " + output);
407         }
408         waitUntilCurrentUser(userId);
409     }
410 
411     /**
412      * Waits until the given user is the current foreground user.
413      */
waitUntilCurrentUser(int userId)414     protected void waitUntilCurrentUser(int userId) throws Exception {
415         CommonTestUtils.waitUntil("timed out (" + DEFAULT_TIMEOUT_SEC
416                 + "s) waiting for current user to be " + userId
417                 + " (it is " + getCurrentUserId() + ")", DEFAULT_TIMEOUT_SEC,
418                 () -> (getCurrentUserId() == userId));
419     }
420 
421     /**
422      * Waits until the current user is not the system user.
423      */
waitUntilCurrentUserIsNotSystem(int timeoutSec)424     protected  void waitUntilCurrentUserIsNotSystem(int timeoutSec) throws Exception {
425         CommonTestUtils.waitUntil("timed out (" + timeoutSec + "s) waiting for current user to NOT "
426                 + "be the system user", timeoutSec,  () -> getCurrentUserId() != SYSTEM_USER_ID);
427     }
428 
429     /**
430      * Waits until {@code n} persistent (i.e., non-ephemeral) users are available.
431      */
waitUntilAtLeastNPersistentUsersAreAvailable(int timeoutSec, int n)432     protected void waitUntilAtLeastNPersistentUsersAreAvailable(int timeoutSec, int n)
433             throws Exception {
434         waitUntil(timeoutSec, () -> getAllPersistentUsers().size() >= n, "%d persistent users", n);
435     }
436 
437     /**
438      * Waits until the given condition is reached.
439      */
waitUntil(long timeoutSeconds, BooleanSupplierWithThrow<Exception> predicate, String msgPattern, Object... msgArgs)440     protected void waitUntil(long timeoutSeconds, BooleanSupplierWithThrow<Exception> predicate,
441             String msgPattern, Object... msgArgs) throws Exception {
442         CommonTestUtils.waitUntil("timed out (" + timeoutSeconds + "s) waiting for "
443                 + String.format(msgPattern, msgArgs), timeoutSeconds, predicate);
444     }
445 
446     // TODO(b/230500604): refactor other CommonTestUtils.waitUntil() calls to use this one insteads
447     /**
448      * Waits until the given condition is reached, using the default timeout.
449      */
waitUntil(BooleanSupplierWithThrow<Exception> predicate, String msgPattern, Object... msgArgs)450     protected void waitUntil(BooleanSupplierWithThrow<Exception> predicate,
451             String msgPattern, Object... msgArgs) throws Exception {
452         waitUntil(DEFAULT_TIMEOUT_SEC, predicate, msgPattern, msgArgs);
453     }
454 
455     /**
456      * Removes a user by user ID and update the list of users to be removed.
457      */
removeUser(int userId)458     protected boolean removeUser(int userId) throws Exception {
459         String result = executeCommand("cmd car_service remove-user %d", userId);
460         if (result.contains("STATUS_SUCCESSFUL")) {
461             return true;
462         }
463         return false;
464     }
465 
466     /**
467      * Removes users whose name start with the given prefix.
468      */
removeUsers(String prefix)469     protected void removeUsers(String prefix) throws Exception {
470         Pattern pattern = Pattern.compile("^.*id=(\\d+), name=(" + prefix + ".*),.*$");
471         String output = executeCommand("cmd user list --all -v");
472         for (String line : output.split("\\n")) {
473             Matcher matcher = pattern.matcher(line);
474             if (!matcher.find()) continue;
475 
476             int userId = Integer.parseInt(matcher.group(1));
477             String name = matcher.group(2);
478             CLog.e("Removing user with %s prefix (id=%d, name='%s')", prefix, userId, name);
479             removeUser(userId);
480         }
481     }
482 
483     /**
484      * Checks if an app is installed for a given user.
485      */
isAppInstalledForUser(String packageName, int userId)486     protected boolean isAppInstalledForUser(String packageName, int userId)
487             throws DeviceNotAvailableException {
488         return getDevice().isPackageInstalled(packageName, Integer.toString(userId));
489     }
490 
491     /**
492      * Fails the test if the app is installed for the given user.
493      */
assertAppInstalledForUser(String packageName, int userId)494     protected void assertAppInstalledForUser(String packageName, int userId)
495             throws DeviceNotAvailableException {
496         assertWithMessage("%s should BE installed for user %s", packageName, userId).that(
497                 isAppInstalledForUser(packageName, userId)).isTrue();
498     }
499 
500     /**
501      * Fails the test if the app is NOT installed for the given user.
502      */
assertAppNotInstalledForUser(String packageName, int userId)503     protected void assertAppNotInstalledForUser(String packageName, int userId)
504             throws DeviceNotAvailableException {
505         assertWithMessage("%s should NOT be installed for user %s", packageName, userId).that(
506                 isAppInstalledForUser(packageName, userId)).isFalse();
507     }
508 
509     /**
510      * Restarts the system server process.
511      *
512      * <p>Useful for cases where the test case changes system properties, as
513      * {@link ITestDevice#reboot()} would reset them.
514      */
restartSystemServer()515     protected void restartSystemServer() throws Exception {
516         long uptimeBefore = getSystemServerUptime();
517         CLog.d("Uptime before restart: %d", uptimeBefore);
518 
519         restartOrReboot();
520 
521         getDevice().waitForDeviceAvailable();
522 
523         // Also checks for uptime - it might be an overkill, but at least it will add more logs,
524         // which could help in case of issues
525         CommonTestUtils.waitUntil("timed out waiting until for new system server uptime",
526                 SYSTEM_RESTART_TIMEOUT_SEC, () -> {
527                     long uptimeAfter = getSystemServerUptime();
528                     CLog.d("Uptime after restart: %d", uptimeAfter);
529                     return uptimeAfter != -1 && uptimeAfter != uptimeBefore;
530                 });
531 
532         waitForCarServiceReady();
533     }
534 
restartOrReboot()535     private void restartOrReboot() throws DeviceNotAvailableException {
536         ITestDevice device = getDevice();
537 
538         if (device.isAdbRoot()) {
539             CLog.d("Restarting system server");
540             device.executeShellCommand("stop");
541             device.executeShellCommand("start");
542             return;
543         }
544 
545         CLog.d("Only root user can restart system server; rebooting instead");
546         getDevice().reboot();
547     }
548 
549     /**
550      * Gets the system server uptime (or {@code -1} if not available).
551      */
getSystemServerUptime()552     protected long getSystemServerUptime() throws DeviceNotAvailableException {
553         return getDevice().getIntProperty("sys.system_server.start_uptime", -1);
554     }
555 
556     /**
557      * Reboots the device.
558      */
reboot()559     protected void reboot() throws Exception {
560         CLog.d("Rebooting device");
561         getDevice().reboot();
562     }
563 
564     /**
565      * Gets mapping of package and permissions granted for requested user id.
566      *
567      * @return Map<String, List<String>> where key is the package name and
568      * the value is list of permissions granted for this user.
569      */
getPackagesAndPermissionsForUser(int userId)570     protected Map<String, List<String>> getPackagesAndPermissionsForUser(int userId)
571             throws Exception {
572         CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
573         getDevice().executeShellCommand("dumpsys package --proto", receiver);
574 
575         PackageServiceDumpProto dump = PackageServiceDumpProto.parser()
576                 .parseFrom(receiver.getOutput());
577 
578         CLog.v("Device has %d packages while getPackagesAndPermissions", dump.getPackagesCount());
579         Map<String, List<String>> pkgMap = new HashMap<>();
580         for (PackageProto pkg : dump.getPackagesList()) {
581             String pkgName = pkg.getName();
582             for (UserPermissionsProto userPermissions : pkg.getUserPermissionsList()) {
583                 if (userPermissions.getId() == userId) {
584                     pkgMap.put(pkg.getName(), userPermissions.getGrantedPermissionsList());
585                     break;
586                 }
587             }
588         }
589         return pkgMap;
590     }
591 
592     /**
593      * Checks if the given package has a process running on the device.
594      */
isPackageRunning(String packageName)595     protected boolean isPackageRunning(String packageName) throws Exception {
596         return !executeCommand("pidof %s", packageName).isEmpty();
597     }
598 
599     /**
600      * Sleeps for the given amount of milliseconds.
601      */
sleep(long ms)602     protected void sleep(long ms) throws InterruptedException {
603         CLog.v("Sleeping for %dms", ms);
604         Thread.sleep(ms);
605         CLog.v("Woke up; Little Susie woke up!");
606     }
607 
608     // TODO(b/169341308): move to common infra code
609     private static final class RequiredFeatureRule implements TestRule {
610 
611         private final ITestInformationReceiver mReceiver;
612         private final String mFeature;
613 
RequiredFeatureRule(ITestInformationReceiver receiver, String feature)614         RequiredFeatureRule(ITestInformationReceiver receiver, String feature) {
615             mReceiver = receiver;
616             mFeature = feature;
617         }
618 
619         @Override
apply(Statement base, Description description)620         public Statement apply(Statement base, Description description) {
621             return new Statement() {
622 
623                 @Override
624                 public void evaluate() throws Throwable {
625                     boolean hasFeature = false;
626                     try {
627                         hasFeature = mReceiver.getTestInformation().getDevice()
628                                 .hasFeature(mFeature);
629                     } catch (DeviceNotAvailableException e) {
630                         CLog.e("Could not check if device has feature %s: %e", mFeature, e);
631                         return;
632                     }
633 
634                     if (!hasFeature) {
635                         CLog.d("skipping %s#%s"
636                                 + " because device does not have feature '%s'",
637                                 description.getClassName(), description.getMethodName(), mFeature);
638                         throw new AssumptionViolatedException("Device does not have feature '"
639                                 + mFeature + "'");
640                     }
641                     base.evaluate();
642                 }
643             };
644         }
645 
646         @Override
toString()647         public String toString() {
648             return "RequiredFeatureRule[" + mFeature + "]";
649         }
650     }
651 
652     /**
653      * Represents a user as returned by {@code cmd user list -v}.
654      */
655     public static final class UserInfo {
656         public final int id;
657         public final String flags;
658         public final String name;
659         public final String otherState;
660 
661         private UserInfo(Matcher matcher) {
662             id = Integer.parseInt(matcher.group(USER_PATTERN_GROUP_ID));
663             flags = matcher.group(USER_PATTERN_GROUP_FLAGS);
664             name = matcher.group(USER_PATTERN_GROUP_NAME);
665             otherState = matcher.group(USER_PATTERN_GROUP_OTHER_STATE);
666         }
667 
668         @Override
669         public String toString() {
670             return "[UserInfo: id=" + id + ", flags=" + flags + ", name=" + name
671                     + ", otherState=" + otherState + "]";
672         }
673     }
674 }
675