1 /*
2  * Copyright (C) 2014 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.cts.devicepolicy;
18 
19 import com.android.cts.migration.MigrationHelper;
20 import com.android.ddmlib.Log.LogLevel;
21 import com.android.ddmlib.testrunner.InstrumentationResultParser;
22 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
23 import com.android.ddmlib.testrunner.TestIdentifier;
24 import com.android.ddmlib.testrunner.TestResult;
25 import com.android.ddmlib.testrunner.TestResult.TestStatus;
26 import com.android.ddmlib.testrunner.TestRunResult;
27 import com.android.tradefed.build.IBuildInfo;
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.log.LogUtil.CLog;
31 import com.android.tradefed.result.CollectingTestListener;
32 import com.android.tradefed.testtype.DeviceTestCase;
33 import com.android.tradefed.testtype.IBuildReceiver;
34 
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.Map;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 
45 import javax.annotation.Nullable;
46 
47 /**
48  * Base class for device policy tests. It offers utility methods to run tests, set device or profile
49  * owner, etc.
50  */
51 public class BaseDevicePolicyTest extends DeviceTestCase implements IBuildReceiver {
52 
53     private static final String RUNNER = "android.support.test.runner.AndroidJUnitRunner";
54 
55     protected static final int USER_SYSTEM = 0; // From the UserHandle class.
56 
57     protected static final int USER_OWNER = 0;
58 
59     // From the UserInfo class
60     protected static final int FLAG_PRIMARY = 0x00000001;
61     protected static final int FLAG_GUEST = 0x00000004;
62     protected static final int FLAG_EPHEMERAL = 0x00000100;
63 
64     protected static interface Settings {
65         public static final String GLOBAL_NAMESPACE = "global";
66         public static interface Global {
67             public static final String DEVICE_PROVISIONED = "device_provisioned";
68         }
69     }
70 
71     protected IBuildInfo mCtsBuild;
72 
73     private String mPackageVerifier;
74     private HashSet<String> mAvailableFeatures;
75 
76     /** Whether DPM is supported. */
77     protected boolean mHasFeature;
78     protected int mPrimaryUserId;
79 
80     /** Whether multi-user is supported. */
81     protected boolean mSupportsMultiUser;
82 
83     /** Users we shouldn't delete in the tests */
84     private ArrayList<Integer> mFixedUsers;
85 
86     @Override
setBuild(IBuildInfo buildInfo)87     public void setBuild(IBuildInfo buildInfo) {
88         mCtsBuild = buildInfo;
89     }
90 
91     @Override
setUp()92     protected void setUp() throws Exception {
93         super.setUp();
94         assertNotNull(mCtsBuild);  // ensure build has been set before test is run.
95         mHasFeature = getDevice().getApiLevel() >= 21 /* Build.VERSION_CODES.L */
96                 && hasDeviceFeature("android.software.device_admin");
97         mSupportsMultiUser = getMaxNumberOfUsersSupported() > 1;
98 
99         // disable the package verifier to avoid the dialog when installing an app
100         mPackageVerifier = getDevice().executeShellCommand(
101                 "settings get global package_verifier_enable");
102         getDevice().executeShellCommand("settings put global package_verifier_enable 0");
103 
104         mFixedUsers = new ArrayList();
105         mPrimaryUserId = getPrimaryUser();
106         mFixedUsers.add(mPrimaryUserId);
107         if (mPrimaryUserId != USER_SYSTEM) {
108             mFixedUsers.add(USER_SYSTEM);
109         }
110         switchUser(mPrimaryUserId);
111         removeOwners();
112         removeTestUsers();
113     }
114 
115     @Override
tearDown()116     protected void tearDown() throws Exception {
117         // reset the package verifier setting to its original value
118         getDevice().executeShellCommand("settings put global package_verifier_enable "
119                 + mPackageVerifier);
120         removeOwners();
121         removeTestUsers();
122         super.tearDown();
123     }
124 
installAppAsUser(String appFileName, int userId)125     protected void installAppAsUser(String appFileName, int userId) throws FileNotFoundException,
126             DeviceNotAvailableException {
127         installAppAsUser(appFileName, true, userId);
128     }
129 
installAppAsUser(String appFileName, boolean grantPermissions, int userId)130     protected void installAppAsUser(String appFileName, boolean grantPermissions, int userId)
131             throws FileNotFoundException, DeviceNotAvailableException {
132         CLog.d("Installing app " + appFileName + " for user " + userId);
133         String result = getDevice().installPackageForUser(
134                 MigrationHelper.getTestFile(mCtsBuild, appFileName), true, grantPermissions,
135                 userId, "-t");
136         assertNull("Failed to install " + appFileName + " for user " + userId + ": " + result,
137                 result);
138     }
139 
140     /** Initializes the user with the given id. This is required so that apps can run on it. */
startUser(int userId)141     protected void startUser(int userId) throws Exception {
142         getDevice().startUser(userId);
143     }
144 
switchUser(int userId)145     protected void switchUser(int userId) throws Exception {
146         // TODO Move this logic to ITestDevice
147         String command = "am switch-user " + userId;
148         CLog.d("Starting command " + command);
149         String commandOutput = getDevice().executeShellCommand(command);
150         CLog.d("Output for command " + command + ": " + commandOutput);
151     }
152 
getMaxNumberOfUsersSupported()153     protected int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
154         return getDevice().getMaxNumberOfUsersSupported();
155     }
156 
getUserFlags(int userId)157     protected int getUserFlags(int userId) throws DeviceNotAvailableException {
158         String command = "pm list users";
159         String commandOutput = getDevice().executeShellCommand(command);
160         CLog.i("Output for command " + command + ": " + commandOutput);
161 
162         String[] lines = commandOutput.split("\\r?\\n");
163         assertTrue(commandOutput + " should contain at least one line", lines.length >= 1);
164         for (int i = 1; i < lines.length; i++) {
165             // Individual user is printed out like this:
166             // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
167             String[] tokens = lines[i].split("\\{|\\}|:");
168             assertTrue(lines[i] + " doesn't contain 4 or 5 tokens",
169                     tokens.length == 4 || tokens.length == 5);
170             // If the user IDs match, return the flags.
171             if (Integer.parseInt(tokens[1]) == userId) {
172                 return Integer.parseInt(tokens[3], 16);
173             }
174         }
175         fail("User not found");
176         return 0;
177     }
178 
listUsers()179     protected ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
180         return getDevice().listUsers();
181     }
182 
stopUser(int userId)183     protected void stopUser(int userId) throws Exception  {
184         String stopUserCommand = "am stop-user -w -f " + userId;
185         CLog.d("starting command \"" + stopUserCommand + "\" and waiting.");
186         CLog.d("Output for command " + stopUserCommand + ": "
187                 + getDevice().executeShellCommand(stopUserCommand));
188     }
189 
removeUser(int userId)190     protected void removeUser(int userId) throws Exception  {
191         if (listUsers().contains(userId) && userId != USER_SYSTEM) {
192             // Don't log output, as tests sometimes set no debug user restriction, which
193             // causes this to fail, we should still continue and remove the user.
194             String stopUserCommand = "am stop-user -w -f " + userId;
195             CLog.d("stopping and removing user " + userId);
196             getDevice().executeShellCommand(stopUserCommand);
197             assertTrue("Couldn't remove user", getDevice().removeUser(userId));
198         }
199     }
200 
removeTestUsers()201     protected void removeTestUsers() throws Exception {
202         for (int userId : listUsers()) {
203             if (!mFixedUsers.contains(userId)) {
204                 removeUser(userId);
205             }
206         }
207     }
208 
209     /** Returns true if the specified tests passed. Tests are run as given user. */
runDeviceTestsAsUser( String pkgName, @Nullable String testClassName, int userId)210     protected boolean runDeviceTestsAsUser(
211             String pkgName, @Nullable String testClassName, int userId)
212             throws DeviceNotAvailableException {
213         return runDeviceTestsAsUser(pkgName, testClassName, null /*testMethodName*/, userId);
214     }
215 
216     /** Returns true if the specified tests passed. Tests are run as given user. */
runDeviceTestsAsUser( String pkgName, @Nullable String testClassName, String testMethodName, int userId)217     protected boolean runDeviceTestsAsUser(
218             String pkgName, @Nullable String testClassName, String testMethodName, int userId)
219             throws DeviceNotAvailableException {
220         Map<String, String> params = Collections.emptyMap();
221         return runDeviceTestsAsUser(pkgName, testClassName, testMethodName, userId, params);
222     }
223 
runDeviceTestsAsUser(String pkgName, @Nullable String testClassName, @Nullable String testMethodName, int userId, Map<String, String> params)224     protected boolean runDeviceTestsAsUser(String pkgName, @Nullable String testClassName,
225             @Nullable String testMethodName, int userId,
226             Map<String, String> params) throws DeviceNotAvailableException {
227         if (testClassName != null && testClassName.startsWith(".")) {
228             testClassName = pkgName + testClassName;
229         }
230 
231         RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(
232                 pkgName, RUNNER, getDevice().getIDevice());
233         if (testClassName != null && testMethodName != null) {
234             testRunner.setMethodName(testClassName, testMethodName);
235         } else if (testClassName != null) {
236             testRunner.setClassName(testClassName);
237         }
238 
239         for (Map.Entry<String, String> param : params.entrySet()) {
240             testRunner.addInstrumentationArg(param.getKey(), param.getValue());
241         }
242 
243         CollectingTestListener listener = new CollectingTestListener();
244         assertTrue(getDevice().runInstrumentationTestsAsUser(testRunner, userId, listener));
245 
246         TestRunResult runResult = listener.getCurrentRunResults();
247         printTestResult(runResult);
248         return !runResult.hasFailedTests() && runResult.getNumTestsInState(TestStatus.PASSED) > 0;
249     }
250 
251     /** Returns true if the system supports the split between system and primary user. */
hasUserSplit()252     protected boolean hasUserSplit() throws DeviceNotAvailableException {
253         return getBooleanSystemProperty("ro.fw.system_user_split", false);
254     }
255 
256     /** Returns a boolean value of the system property with the specified key. */
getBooleanSystemProperty(String key, boolean defaultValue)257     protected boolean getBooleanSystemProperty(String key, boolean defaultValue)
258             throws DeviceNotAvailableException {
259         final String[] positiveValues = {"1", "y", "yes", "true", "on"};
260         final String[] negativeValues = {"0", "n", "no", "false", "off"};
261         String propertyValue = getDevice().getProperty(key);
262         if (propertyValue == null || propertyValue.isEmpty()) {
263             return defaultValue;
264         }
265         if (Arrays.asList(positiveValues).contains(propertyValue)) {
266             return true;
267         }
268         if (Arrays.asList(negativeValues).contains(propertyValue)) {
269             return false;
270         }
271         fail("Unexpected value of boolean system property '" + key + "': " + propertyValue);
272         return false;
273     }
274 
275     /** Checks whether it is possible to create the desired number of users. */
canCreateAdditionalUsers(int numberOfUsers)276     protected boolean canCreateAdditionalUsers(int numberOfUsers)
277             throws DeviceNotAvailableException {
278         return listUsers().size() + numberOfUsers <= getMaxNumberOfUsersSupported();
279     }
280 
printTestResult(TestRunResult runResult)281     private void printTestResult(TestRunResult runResult) {
282         for (Map.Entry<TestIdentifier, TestResult> testEntry :
283                 runResult.getTestResults().entrySet()) {
284             TestResult testResult = testEntry.getValue();
285             CLog.d("Test " + testEntry.getKey() + ": " + testResult.getStatus());
286             if (testResult.getStatus() != TestStatus.PASSED) {
287                 CLog.d(testResult.getStackTrace());
288             }
289         }
290     }
291 
hasDeviceFeature(String requiredFeature)292     protected boolean hasDeviceFeature(String requiredFeature) throws DeviceNotAvailableException {
293         if (mAvailableFeatures == null) {
294             // TODO: Move this logic to ITestDevice.
295             String command = "pm list features";
296             String commandOutput = getDevice().executeShellCommand(command);
297             CLog.i("Output for command " + command + ": " + commandOutput);
298 
299             // Extract the id of the new user.
300             mAvailableFeatures = new HashSet<>();
301             for (String feature: commandOutput.split("\\s+")) {
302                 // Each line in the output of the command has the format "feature:{FEATURE_VALUE}".
303                 String[] tokens = feature.split(":");
304                 assertTrue("\"" + feature + "\" expected to have format feature:{FEATURE_VALUE}",
305                         tokens.length > 1);
306                 assertEquals(feature, "feature", tokens[0]);
307                 mAvailableFeatures.add(tokens[1]);
308             }
309         }
310         boolean result = mAvailableFeatures.contains(requiredFeature);
311         if (!result) {
312             CLog.d("Device doesn't have required feature "
313             + requiredFeature + ". Test won't run.");
314         }
315         return result;
316     }
317 
createUser()318     protected int createUser() throws Exception {
319         int userId = createUser(0);
320         // TODO remove this and audit tests so they start users as necessary
321         startUser(userId);
322         return userId;
323     }
324 
createUser(int flags)325     protected int createUser(int flags) throws Exception {
326         boolean guest = FLAG_GUEST == (flags & FLAG_GUEST);
327         boolean ephemeral = FLAG_EPHEMERAL == (flags & FLAG_EPHEMERAL);
328         // TODO Use ITestDevice.createUser() when guest and ephemeral is available
329         String command ="pm create-user " + (guest ? "--guest " : "")
330                 + (ephemeral ? "--ephemeral " : "") + "TestUser_" + System.currentTimeMillis();
331         CLog.d("Starting command " + command);
332         String commandOutput = getDevice().executeShellCommand(command);
333         CLog.d("Output for command " + command + ": " + commandOutput);
334 
335         // Extract the id of the new user.
336         String[] tokens = commandOutput.split("\\s+");
337         assertTrue(tokens.length > 0);
338         assertEquals("Success:", tokens[0]);
339         return Integer.parseInt(tokens[tokens.length-1]);
340     }
341 
createManagedProfile(int parentUserId)342     protected int createManagedProfile(int parentUserId) throws DeviceNotAvailableException {
343         String command = "pm create-user --profileOf " + parentUserId + " --managed "
344                 + "TestProfile_" + System.currentTimeMillis();
345         CLog.d("Starting command " + command);
346         String commandOutput = getDevice().executeShellCommand(command);
347         CLog.d("Output for command " + command + ": " + commandOutput);
348 
349         // Extract the id of the new user.
350         String[] tokens = commandOutput.split("\\s+");
351         assertTrue(commandOutput + " expected to have format \"Success: {USER_ID}\"",
352                 tokens.length > 0);
353         assertEquals(commandOutput, "Success:", tokens[0]);
354         return Integer.parseInt(tokens[tokens.length-1]);
355     }
356 
getPrimaryUser()357     protected int getPrimaryUser() throws DeviceNotAvailableException {
358         return getDevice().getPrimaryUserId();
359     }
360 
getUserSerialNumber(int userId)361     protected int getUserSerialNumber(int userId) throws DeviceNotAvailableException{
362         // TODO: Move this logic to ITestDevice.
363         // dumpsys user return lines like "UserInfo{0:Owner:13} serialNo=0"
364         String commandOutput = getDevice().executeShellCommand("dumpsys user");
365         String[] tokens = commandOutput.split("\\n");
366         for (String token : tokens) {
367             token = token.trim();
368             if (token.contains("UserInfo{" + userId + ":")) {
369                 String[] split = token.split("serialNo=");
370                 assertTrue(split.length == 2);
371                 int serialNumber = Integer.parseInt(split[1]);
372                 CLog.d("Serial number of user " + userId + ": "
373                         + serialNumber);
374                 return serialNumber;
375             }
376         }
377         fail("Couldn't find user " + userId);
378         return -1;
379     }
380 
setProfileOwner(String componentName, int userId, boolean expectFailure)381     protected boolean setProfileOwner(String componentName, int userId, boolean expectFailure)
382             throws DeviceNotAvailableException {
383         String command = "dpm set-profile-owner --user " + userId + " '" + componentName + "'";
384         String commandOutput = getDevice().executeShellCommand(command);
385         boolean success = commandOutput.startsWith("Success:");
386         // If we succeeded always log, if we are expecting failure don't log failures
387         // as call stacks for passing tests confuse the logs.
388         if (success || !expectFailure) {
389             CLog.d("Output for command " + command + ": " + commandOutput);
390         } else {
391             CLog.d("Command Failed " + command);
392         }
393         return success;
394     }
395 
setProfileOwnerOrFail(String componentName, int userId)396     protected void setProfileOwnerOrFail(String componentName, int userId)
397             throws Exception {
398         if (!setProfileOwner(componentName, userId, /*expectFailure*/ false)) {
399             removeUser(userId);
400             fail("Failed to set profile owner");
401         }
402     }
403 
setDeviceAdminInner(String componentName, int userId)404     private String setDeviceAdminInner(String componentName, int userId)
405             throws DeviceNotAvailableException {
406         String command = "dpm set-active-admin --user " + userId + " '" + componentName + "'";
407         String commandOutput = getDevice().executeShellCommand(command);
408         return commandOutput;
409     }
410 
setDeviceAdmin(String componentName, int userId)411     protected void setDeviceAdmin(String componentName, int userId)
412             throws DeviceNotAvailableException {
413         String commandOutput = setDeviceAdminInner(componentName, userId);
414         CLog.d("Output for command " + commandOutput
415                 + ": " + commandOutput);
416         assertTrue(commandOutput + " expected to start with \"Success:\"",
417                 commandOutput.startsWith("Success:"));
418     }
419 
setDeviceAdminExpectingFailure(String componentName, int userId, String errorMessage)420     protected void setDeviceAdminExpectingFailure(String componentName, int userId,
421             String errorMessage) throws DeviceNotAvailableException {
422         String commandOutput = setDeviceAdminInner(componentName, userId);
423         if (!commandOutput.contains(errorMessage)) {
424             fail(commandOutput + " expected to contain \"" + errorMessage + "\"");
425         }
426     }
427 
setDeviceOwner(String componentName, int userId, boolean expectFailure)428     protected boolean setDeviceOwner(String componentName, int userId, boolean expectFailure)
429             throws DeviceNotAvailableException {
430         String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
431         String commandOutput = getDevice().executeShellCommand(command);
432         boolean success = commandOutput.startsWith("Success:");
433         // If we succeeded always log, if we are expecting failure don't log failures
434         // as call stacks for passing tests confuse the logs.
435         if (success || !expectFailure) {
436             CLog.d("Output for command " + command + ": " + commandOutput);
437         } else {
438             CLog.d("Command Failed " + command);
439         }
440         return success;
441     }
442 
getSettings(String namespace, String name, int userId)443     protected String getSettings(String namespace, String name, int userId)
444             throws DeviceNotAvailableException {
445         String command = "settings --user " + userId + " get " + namespace + " " + name;
446         String commandOutput = getDevice().executeShellCommand(command);
447         CLog.d("Output for command " + command + ": " + commandOutput);
448         return commandOutput.replace("\n", "").replace("\r", "");
449     }
450 
putSettings(String namespace, String name, String value, int userId)451     protected void putSettings(String namespace, String name, String value, int userId)
452             throws DeviceNotAvailableException {
453         String command = "settings --user " + userId + " put " + namespace + " " + name
454                 + " " + value;
455         String commandOutput = getDevice().executeShellCommand(command);
456         CLog.d("Output for command " + command + ": " + commandOutput);
457     }
458 
removeAdmin(String componentName, int userId)459     protected boolean removeAdmin(String componentName, int userId)
460             throws DeviceNotAvailableException {
461         String command = "dpm remove-active-admin --user " + userId + " '" + componentName + "'";
462         String commandOutput = getDevice().executeShellCommand(command);
463         CLog.d("Output for command " + command + ": " + commandOutput);
464         return commandOutput.startsWith("Success:");
465     }
466 
467     // Tries to remove and profile or device owners it finds.
removeOwners()468     protected void removeOwners() throws DeviceNotAvailableException {
469         String command = "dumpsys device_policy";
470         String commandOutput = getDevice().executeShellCommand(command);
471         String[] lines = commandOutput.split("\\r?\\n");
472         for (int i = 0; i < lines.length; ++i) {
473             String line = lines[i].trim();
474             if (line.contains("Profile Owner")) {
475                 // Line is "Profile owner (User <id>):
476                 String[] tokens = line.split("\\(|\\)| ");
477                 int userId = Integer.parseInt(tokens[4]);
478                 i++;
479                 line = lines[i].trim();
480                 // Line is admin=ComponentInfo{<component>}
481                 tokens = line.split("\\{|\\}");
482                 String componentName = tokens[1];
483                 CLog.w("Cleaning up profile owner " + userId + " " + componentName);
484                 removeAdmin(componentName, userId);
485             } else if (line.contains("Device Owner:")) {
486                 i++;
487                 line = lines[i].trim();
488                 // Line is admin=ComponentInfo{<component>}
489                 String[] tokens = line.split("\\{|\\}");
490                 String componentName = tokens[1];
491                 // Skip to user id line.
492                 i += 3;
493                 line = lines[i].trim();
494                 // Line is User ID: <N>
495                 tokens = line.split(":");
496                 int userId = Integer.parseInt(tokens[1].trim());
497                 CLog.w("Cleaning up device owner " + userId + " " + componentName);
498                 removeAdmin(componentName, userId);
499             }
500         }
501     }
502 }
503