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.tradefed.device.CollectingByteOutputReceiver;
30 import com.android.tradefed.device.DeviceNotAvailableException;
31 import com.android.tradefed.device.ITestDevice;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.testtype.ITestInformationReceiver;
34 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
35 
36 import org.junit.After;
37 import org.junit.AssumptionViolatedException;
38 import org.junit.Before;
39 import org.junit.Rule;
40 import org.junit.rules.TestRule;
41 import org.junit.runner.Description;
42 import org.junit.runners.model.Statement;
43 
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.function.Function;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 
53 /**
54  * Base class for all test cases.
55  */
56 // NOTE: must be public because of @Rules
57 public abstract class CarHostJUnit4TestCase extends BaseHostJUnit4Test {
58 
59     private static final int DEFAULT_TIMEOUT_SEC = 20;
60 
61     private static final Pattern CREATE_USER_OUTPUT_PATTERN = Pattern.compile("id=(\\d+)");
62 
63     private static final String USER_PREFIX = "CtsCarHostTestCases";
64 
65     /**
66      * User pattern in the output of "cmd user list --all -v"
67      * TEXT id=<id> TEXT name=<name>, TEX flags=<flags> TEXT
68      * group 1: id group 2: name group 3: flags group 4: other state(like "(running)")
69      */
70     private static final Pattern USER_PATTERN = Pattern.compile(
71             ".*id=(\\d+).*name=([^\\s,]+).*flags=(\\S+)(.*)");
72 
73     private static final int USER_PATTERN_GROUP_ID = 1;
74     private static final int USER_PATTERN_GROUP_NAME = 2;
75     private static final int USER_PATTERN_GROUP_FLAGS = 3;
76     private static final int USER_PATTERN_GROUP_OTHER_STATE = 4;
77 
78     /**
79      * User's package permission pattern string format in the output of "dumpsys package PKG_NAME"
80     */
81     protected static final String APP_APK = "CtsCarApp.apk";
82     protected static final String APP_PKG = "android.car.cts.app";
83 
84     @Rule
85     public final RequiredFeatureRule mHasAutomotiveRule = new RequiredFeatureRule(this,
86             "android.hardware.type.automotive");
87 
88     private final HashSet<Integer> mUsersToBeRemoved = new HashSet<>();
89 
90     private int mInitialUserId;
91     private Integer mInitialMaximumNumberOfUsers;
92 
93     /**
94      * Saves multi-user state so it can be restored after the test.
95      */
96     @Before
saveUserState()97     public void saveUserState() throws Exception {
98         removeUsers(USER_PREFIX);
99 
100         mInitialUserId = getCurrentUserId();
101     }
102 
103     /**
104      * Restores multi-user state from before the test.
105      */
106     @After
restoreUsersState()107     public void restoreUsersState() throws Exception {
108         int currentUserId = getCurrentUserId();
109         CLog.d("restoreUsersState(): initial user: %d, current user: %d, created users: %s "
110                 + "max number of users: %d",
111                 mInitialUserId, currentUserId, mUsersToBeRemoved, mInitialMaximumNumberOfUsers);
112         if (currentUserId != mInitialUserId) {
113             CLog.i("Switching back from %d to %d", currentUserId, mInitialUserId);
114             switchUser(mInitialUserId);
115         }
116 
117         if (!mUsersToBeRemoved.isEmpty()) {
118             CLog.i("Removing users %s", mUsersToBeRemoved);
119             for (int userId : mUsersToBeRemoved) {
120                 removeUser(userId);
121             }
122         }
123 
124         // Should have been removed above, but as the saying goes, better safe than sorry...
125         removeUsers(USER_PREFIX);
126 
127         if (mInitialMaximumNumberOfUsers != null) {
128             CLog.i("Restoring max number of users to %d", mInitialMaximumNumberOfUsers);
129             setMaxNumberUsers(mInitialMaximumNumberOfUsers);
130         }
131     }
132 
133     /**
134      * Makes sure the device supports multiple users, throwing {@link AssumptionViolatedException}
135      * if it doesn't.
136      */
assumeSupportsMultipleUsers()137     protected final void assumeSupportsMultipleUsers() throws Exception {
138         assumeTrue("device does not support multi-user",
139                 getDevice().getMaxNumberOfUsersSupported() > 1);
140     }
141 
142     /**
143      * Makes sure the device can add {@code numberOfUsers} new users, increasing limit if needed or
144      * failing if not possible.
145      */
requiresExtraUsers(int numberOfUsers)146     protected final void requiresExtraUsers(int numberOfUsers) throws Exception {
147         assumeSupportsMultipleUsers();
148 
149         int maxNumber = getDevice().getMaxNumberOfUsersSupported();
150         int currentNumber = getDevice().listUsers().size();
151 
152         if (currentNumber + numberOfUsers <= maxNumber) return;
153 
154         if (!getDevice().isAdbRoot()) {
155             failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ false);
156         }
157 
158         // Increase limit...
159         mInitialMaximumNumberOfUsers = maxNumber;
160         setMaxNumberUsers(maxNumber + numberOfUsers);
161 
162         // ...and try again
163         maxNumber = getDevice().getMaxNumberOfUsersSupported();
164         if (currentNumber + numberOfUsers > maxNumber) {
165             failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ true);
166         }
167     }
168 
failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber, boolean isAdbRoot)169     private void failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber,
170             boolean isAdbRoot) {
171         String reason = isAdbRoot ? "failed to increase it"
172                 : "cannot be increased without adb root";
173         String existingUsers = "";
174         try {
175             existingUsers = "Existing users: " + executeCommand("cmd user list --all -v");
176         } catch (Exception e) {
177             // ignore
178         }
179         fail("Cannot create " + numberOfUsers + " users: current number is " + currentNumber
180                 + ", limit is " + maxNumber + " and could not be increased (" + reason + "). "
181                 + existingUsers);
182     }
183 
184     /**
185      * Executes the shell command and returns the output.
186      */
executeCommand(String command, Object... args)187     protected String executeCommand(String command, Object... args) throws Exception {
188         String fullCommand = String.format(command, args);
189         return getDevice().executeShellCommand(fullCommand);
190     }
191 
192     /**
193      * Executes the shell command and parses output with {@code resultParser}.
194      */
executeAndParseCommand(Function<String, T> resultParser, String command, Object... args)195     protected <T> T executeAndParseCommand(Function<String, T> resultParser,
196             String command, Object... args) throws Exception {
197         String output = executeCommand(command, args);
198         return resultParser.apply(output);
199     }
200 
201     /**
202      * Executes the shell command and parses the Matcher output with {@code resultParser}, failing
203      * with {@code matchNotFoundErrorMessage} if it didn't match the {@code regex}.
204      */
executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage, Function<Matcher, T> resultParser, String command, Object... args)205     protected <T> T executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage,
206             Function<Matcher, T> resultParser,
207             String command, Object... args) throws Exception {
208         String output = executeCommand(command, args);
209         Matcher matcher = regex.matcher(output);
210         if (!matcher.find()) {
211             fail(matchNotFoundErrorMessage + ". Shell command: '" + String.format(command, args)
212                     + "'. Output: " + output.trim() + ". Regex: " + regex);
213         }
214         return resultParser.apply(matcher);
215     }
216 
217     /**
218      * Executes the shell command and parses the Matcher output with {@code resultParser}.
219      */
executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser, String command, Object... args)220     protected <T> T executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser,
221             String command, Object... args) throws Exception {
222         String output = executeCommand(command, args);
223         return resultParser.apply(regex.matcher(output));
224     }
225 
226     /**
227      * Executes the shell command that returns all users and returns {@code function} applied to
228      * them.
229      */
onAllUsers(Function<List<UserInfo>, T> function)230     public <T> T onAllUsers(Function<List<UserInfo>, T> function) throws Exception {
231         ArrayList<UserInfo> allUsers = executeAndParseCommand(USER_PATTERN, (matcher) -> {
232             ArrayList<UserInfo> users = new ArrayList<>();
233             while (matcher.find()) {
234                 users.add(new UserInfo(matcher));
235             }
236             return users;
237         }, "cmd user list --all -v");
238         return function.apply(allUsers);
239     }
240 
241     /**
242      * Gets the info for the given user.
243      */
getUserInfo(int userId)244     public UserInfo getUserInfo(int userId) throws Exception {
245         return onAllUsers((allUsers) -> allUsers.stream()
246                 .filter((u) -> u.id == userId))
247                         .findFirst().get();
248     }
249 
250     /**
251      * Sets the maximum number of users that can be created for this car.
252      *
253      * @throws IllegalStateException if adb is not running as root
254      */
setMaxNumberUsers(int numUsers)255     protected void setMaxNumberUsers(int numUsers) throws Exception {
256         if (!getDevice().isAdbRoot()) {
257             throw new IllegalStateException("must be running adb root");
258         }
259         executeCommand("setprop fw.max_users %d", numUsers);
260     }
261 
262     /**
263      * Gets the current user's id.
264      */
getCurrentUserId()265     protected int getCurrentUserId() throws DeviceNotAvailableException {
266         return getDevice().getCurrentUser();
267     }
268 
269     /**
270      * Creates a full user with car service shell command.
271      */
createFullUser(String name)272     protected int createFullUser(String name) throws Exception {
273         return createUser(name, /* flags= */ 0, "android.os.usertype.full.SECONDARY");
274     }
275 
276     /**
277      * Creates a full guest with car service shell command.
278      */
createGuestUser(String name)279     protected int createGuestUser(String name) throws Exception {
280         return createUser(name, /* flags= */ 0, "android.os.usertype.full.GUEST");
281     }
282 
283     /**
284      * Creates a flexible user with car service shell command.
285      *
286      * <p><b>NOTE: </b>it uses User HAL flags, not core Android's.
287      */
createUser(String name, int flags, String type)288     protected int createUser(String name, int flags, String type) throws Exception {
289         name = USER_PREFIX + "." + name;
290         waitForCarServiceReady();
291         int userId = executeAndParseCommand(CREATE_USER_OUTPUT_PATTERN,
292                 "Could not create user with name " + name + ", flags " + flags + ", type" + type,
293                 matcher -> Integer.parseInt(matcher.group(1)),
294                 "cmd car_service create-user --flags %d --type %s %s",
295                 flags, type, name);
296         markUserForRemovalAfterTest(userId);
297         return userId;
298     }
299 
300     /**
301      * Marks a user to be removed at the end of the tests.
302      */
markUserForRemovalAfterTest(int userId)303     protected void markUserForRemovalAfterTest(int userId) {
304         mUsersToBeRemoved.add(userId);
305     }
306 
307     /**
308      * Waits until the given user is initialized.
309      */
waitForUserInitialized(int userId)310     protected void waitForUserInitialized(int userId) throws Exception {
311         CommonTestUtils.waitUntil("timed out waiting for user " + userId + " initialization",
312                 DEFAULT_TIMEOUT_SEC, () -> isUserInitialized(userId));
313     }
314 
315     /**
316      * Waits until the system server is ready.
317      */
waitForCarServiceReady()318     protected void waitForCarServiceReady() throws Exception {
319         CommonTestUtils.waitUntil("timed out waiting for system server ",
320                 DEFAULT_TIMEOUT_SEC, () -> isCarServiceReady());
321     }
322 
isCarServiceReady()323     protected boolean isCarServiceReady() {
324         String cmd = "service check car_service";
325         try {
326             String output = getDevice().executeShellCommand(cmd);
327             return !output.endsWith("not found");
328         } catch (Exception e) {
329             CLog.i("%s failed: %s", cmd, e.getMessage());
330         }
331         return false;
332     }
333 
334     /**
335      * Asserts that the given user is initialized.
336      */
assertUserInitialized(int userId)337     protected void assertUserInitialized(int userId) throws Exception {
338         assertWithMessage("User %s not initialized", userId).that(isUserInitialized(userId))
339                 .isTrue();
340         CLog.v("User %d is initialized", userId);
341     }
342 
343     /**
344      * Checks if the given user is initialized.
345      */
isUserInitialized(int userId)346     protected boolean isUserInitialized(int userId) throws Exception {
347         UserInfo userInfo = getUserInfo(userId);
348         CLog.v("isUserInitialized(%d): %s", userId, userInfo);
349         return userInfo.flags.contains("INITIALIZED");
350     }
351 
352     /**
353      * Switches the current user.
354      */
switchUser(int userId)355     protected void switchUser(int userId) throws Exception {
356         waitForCarServiceReady();
357         String output = executeCommand("cmd car_service switch-user %d", userId);
358         if (!output.contains("STATUS_SUCCESSFUL")) {
359             throw new IllegalStateException("Failed to switch to user " + userId + ": " + output);
360         }
361         waitUntilCurrentUser(userId);
362     }
363 
364     /**
365      * Waits until the given user is the current foreground user.
366      */
waitUntilCurrentUser(int userId)367     protected void waitUntilCurrentUser(int userId) throws Exception {
368         CommonTestUtils.waitUntil("timed out (" + DEFAULT_TIMEOUT_SEC
369                 + "s) waiting for current user to be " + userId
370                 + " (it is " + getCurrentUserId() + ")",
371                 DEFAULT_TIMEOUT_SEC,
372                 () -> (getCurrentUserId() == userId));
373     }
374 
375     /**
376      * Removes a user by user ID and update the list of users to be removed.
377      */
removeUser(int userId)378     protected void removeUser(int userId) throws Exception {
379         executeCommand("cmd car_service remove-user %d", userId);
380     }
381 
382     /**
383      * Removes users whose name start with the given prefix.
384      */
removeUsers(String prefix)385     protected void removeUsers(String prefix) throws Exception {
386         Pattern pattern = Pattern.compile("^.*id=(\\d+), name=(" + prefix + ".*),.*$");
387         String output = executeCommand("cmd user list --all -v");
388         for (String line : output.split("\\n")) {
389             Matcher matcher = pattern.matcher(line);
390             if (!matcher.find()) continue;
391 
392             int userId = Integer.parseInt(matcher.group(1));
393             String name = matcher.group(2);
394             CLog.e("Removing user with %s prefix (id=%d, name='%s')", prefix, userId, name);
395             removeUser(userId);
396         }
397     }
398 
399     /**
400      * Checks if an app is installed for a given user.
401      */
isAppInstalledForUser(String packageName, int userId)402     protected boolean isAppInstalledForUser(String packageName, int userId)
403             throws DeviceNotAvailableException {
404         return getDevice().isPackageInstalled(packageName, Integer.toString(userId));
405     }
406 
407     /**
408      * Fails the test if the app is installed for the given user.
409      */
assertAppInstalledForUser(String packageName, int userId)410     protected void assertAppInstalledForUser(String packageName, int userId)
411             throws DeviceNotAvailableException {
412         assertWithMessage("%s should BE installed for user %s", packageName, userId).that(
413                 isAppInstalledForUser(packageName, userId)).isTrue();
414     }
415 
416     /**
417      * Fails the test if the app is NOT installed for the given user.
418      */
assertAppNotInstalledForUser(String packageName, int userId)419     protected void assertAppNotInstalledForUser(String packageName, int userId)
420             throws DeviceNotAvailableException {
421         assertWithMessage("%s should NOT be installed for user %s", packageName, userId).that(
422                 isAppInstalledForUser(packageName, userId)).isFalse();
423     }
424 
425     /**
426      * Restarts the system server process.
427      *
428      * <p>Useful for cases where the test case changes system properties, as
429      * {@link ITestDevice#reboot()} would reset them.
430      */
restartSystemServer()431     protected void restartSystemServer() throws Exception {
432         final ITestDevice device = getDevice();
433         device.executeShellCommand("stop");
434         device.executeShellCommand("start");
435         device.waitForDeviceAvailable();
436         waitForCarServiceReady();
437     }
438 
439     /**
440      * Gets mapping of package and permissions granted for requested user id.
441      *
442      * @return Map<String, List<String>> where key is the package name and
443      * the value is list of permissions granted for this user.
444      */
getPackagesAndPermissionsForUser(int userId)445     protected Map<String, List<String>> getPackagesAndPermissionsForUser(int userId)
446             throws Exception {
447         CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
448         getDevice().executeShellCommand("dumpsys package --proto", receiver);
449 
450         PackageServiceDumpProto dump = PackageServiceDumpProto.parser()
451                 .parseFrom(receiver.getOutput());
452 
453         CLog.v("Device has %d packages while getPackagesAndPermissions", dump.getPackagesCount());
454         Map<String, List<String>> pkgMap = new HashMap<>();
455         for (PackageProto pkg : dump.getPackagesList()) {
456             String pkgName = pkg.getName();
457             for (UserPermissionsProto userPermissions : pkg.getUserPermissionsList()) {
458                 if (userPermissions.getId() == userId) {
459                     pkgMap.put(pkg.getName(), userPermissions.getGrantedPermissionsList());
460                     break;
461                 }
462             }
463         }
464         return pkgMap;
465     }
466 
467     /**
468      * Sleeps for the given amount of milliseconds.
469      */
sleep(long ms)470     protected void sleep(long ms) throws InterruptedException {
471         CLog.v("Sleeping for %dms", ms);
472         Thread.sleep(ms);
473         CLog.v("Woke up; Little Susie woke up!");
474     }
475 
476     // TODO(b/169341308): move to common infra code
477     private static final class RequiredFeatureRule implements TestRule {
478 
479         private final ITestInformationReceiver mReceiver;
480         private final String mFeature;
481 
RequiredFeatureRule(ITestInformationReceiver receiver, String feature)482         RequiredFeatureRule(ITestInformationReceiver receiver, String feature) {
483             mReceiver = receiver;
484             mFeature = feature;
485         }
486 
487         @Override
apply(Statement base, Description description)488         public Statement apply(Statement base, Description description) {
489             return new Statement() {
490 
491                 @Override
492                 public void evaluate() throws Throwable {
493                     boolean hasFeature = false;
494                     try {
495                         hasFeature = mReceiver.getTestInformation().getDevice()
496                                 .hasFeature(mFeature);
497                     } catch (DeviceNotAvailableException e) {
498                         CLog.e("Could not check if device has feature %s: %e", mFeature, e);
499                         return;
500                     }
501 
502                     if (!hasFeature) {
503                         CLog.d("skipping %s#%s"
504                                 + " because device does not have feature '%s'",
505                                 description.getClassName(), description.getMethodName(), mFeature);
506                         throw new AssumptionViolatedException("Device does not have feature '"
507                                 + mFeature + "'");
508                     }
509                     base.evaluate();
510                 }
511             };
512         }
513 
514         @Override
toString()515         public String toString() {
516             return "RequiredFeatureRule[" + mFeature + "]";
517         }
518     }
519 
520     /**
521      * Represents a user as returned by {@code cmd user list -v}.
522      */
523     public static final class UserInfo {
524         public final int id;
525         public final String flags;
526         public final String name;
527         public final String otherState;
528 
529         private UserInfo(Matcher matcher) {
530             id = Integer.parseInt(matcher.group(USER_PATTERN_GROUP_ID));
531             flags = matcher.group(USER_PATTERN_GROUP_FLAGS);
532             name = matcher.group(USER_PATTERN_GROUP_NAME);
533             otherState = matcher.group(USER_PATTERN_GROUP_OTHER_STATE);
534         }
535 
536         @Override
537         public String toString() {
538             return "[UserInfo: id=" + id + ", flags=" + flags + ", name=" + name
539                     + ", otherState=" + otherState + "]";
540         }
541     }
542 }
543