1 /* 2 * Copyright (C) 2017 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.media.cts; 18 19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 20 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 21 import com.android.ddmlib.testrunner.TestResult.TestStatus; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.device.DeviceNotAvailableException; 24 import com.android.tradefed.log.LogUtil.CLog; 25 import com.android.tradefed.result.CollectingTestListener; 26 import com.android.tradefed.result.TestDescription; 27 import com.android.tradefed.result.TestResult; 28 import com.android.tradefed.result.TestRunResult; 29 import com.android.tradefed.testtype.DeviceTestCase; 30 import com.android.tradefed.testtype.IBuildReceiver; 31 32 import java.io.FileNotFoundException; 33 import java.util.ArrayList; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Set; 38 import java.util.concurrent.TimeUnit; 39 40 import javax.annotation.Nonnull; 41 import javax.annotation.Nullable; 42 43 /** 44 * Base class for host-side tests for multi-user aware media APIs. 45 */ 46 public class BaseMultiUserTest extends DeviceTestCase implements IBuildReceiver { 47 private static final String RUNNER = "android.support.test.runner.AndroidJUnitRunner"; 48 49 /** 50 * The defined timeout (in milliseconds) is used as a maximum waiting time when expecting the 51 * command output from the device. At any time, if the shell command does not output anything 52 * for a period longer than the defined timeout the Tradefed run terminates. 53 */ 54 private static final long DEFAULT_SHELL_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5); 55 56 /** 57 * Instrumentation test runner argument key used for individual test timeout 58 **/ 59 protected static final String TEST_TIMEOUT_INST_ARGS_KEY = "timeout_msec"; 60 61 /** 62 * Sets timeout (in milliseconds) that will be applied to each test. In the 63 * event of a test timeout it will log the results and proceed with executing the next test. 64 */ 65 private static final long DEFAULT_TEST_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5); 66 private static final String SETTINGS_PACKAGE_VERIFIER_NAMESPACE = "global"; 67 private static final String SETTINGS_PACKAGE_VERIFIER_NAME = "package_verifier_enable"; 68 69 /** 70 * User ID for all users. 71 * The value is from the UserHandle class. 72 */ 73 protected static final int USER_ALL = -1; 74 75 /** 76 * User ID for the system user. 77 * The value is from the UserHandle class. 78 */ 79 protected static final int USER_SYSTEM = 0; 80 81 private IBuildInfo mCtsBuild; 82 private String mPackageVerifier; 83 84 private Set<String> mExistingPackages; 85 private List<Integer> mExistingUsers; 86 private HashSet<String> mAvailableFeatures; 87 88 @Override 89 protected void setUp() throws Exception { 90 super.setUp(); 91 // Ensure that build has been set before test is run. 92 assertNotNull(mCtsBuild); 93 mExistingPackages = getDevice().getInstalledPackageNames(); 94 95 // Disable the package verifier to avoid the dialog when installing an app 96 mPackageVerifier = 97 getSettings( 98 SETTINGS_PACKAGE_VERIFIER_NAMESPACE, 99 SETTINGS_PACKAGE_VERIFIER_NAME, 100 USER_ALL); 101 putSettings( 102 SETTINGS_PACKAGE_VERIFIER_NAMESPACE, 103 SETTINGS_PACKAGE_VERIFIER_NAME, 104 "0", 105 USER_ALL); 106 107 mExistingUsers = new ArrayList(); 108 int primaryUserId = getDevice().getPrimaryUserId(); 109 mExistingUsers.add(primaryUserId); 110 mExistingUsers.add(USER_SYSTEM); 111 112 executeShellCommand("am switch-user " + primaryUserId); 113 executeShellCommand("wm dismiss-keyguard"); 114 } 115 116 @Override 117 protected void tearDown() throws Exception { 118 // Reset the package verifier setting to its original value. 119 putSettings( 120 SETTINGS_PACKAGE_VERIFIER_NAMESPACE, 121 SETTINGS_PACKAGE_VERIFIER_NAME, 122 mPackageVerifier, 123 USER_ALL); 124 125 // Remove users created during the test. 126 for (int userId : getDevice().listUsers()) { 127 if (!mExistingUsers.contains(userId)) { 128 removeUser(userId); 129 } 130 } 131 // Remove packages installed during the test. 132 for (String packageName : getDevice().getUninstallablePackageNames()) { 133 if (mExistingPackages.contains(packageName)) { 134 continue; 135 } 136 CLog.d("Removing leftover package: " + packageName); 137 getDevice().uninstallPackage(packageName); 138 } 139 super.tearDown(); 140 } 141 142 @Override 143 public void setBuild(IBuildInfo buildInfo) { 144 mCtsBuild = buildInfo; 145 } 146 147 /** 148 * Installs the app as if the user of the ID {@param userId} has installed the app. 149 * 150 * @param appFileName file name of the app. 151 * @param userId user ID to install the app against. 152 */ 153 protected void installAppAsUser(String appFileName, int userId, boolean asInstantApp) 154 throws FileNotFoundException, DeviceNotAvailableException { 155 CLog.d("Installing app " + appFileName + " for user " + userId); 156 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild); 157 String result = getDevice().installPackageForUser( 158 buildHelper.getTestFile(appFileName), 159 true, 160 true, 161 userId, 162 "-t", 163 asInstantApp ? "--instant" : ""); 164 assertNull("Failed to install " + appFileName + " for user " + userId + ": " + result, 165 result); 166 } 167 168 /** 169 * Excutes shell command and returns the result. 170 * 171 * @param command command to run. 172 * @return result from the command. If the result was {@code null}, empty string ("") will be 173 * returned instead. Otherwise, trimmed result will be returned. 174 */ 175 protected @Nonnull String executeShellCommand(final String command) throws Exception { 176 CLog.d("Starting command " + command); 177 String commandOutput = getDevice().executeShellCommand(command); 178 CLog.d("Output for command " + command + ": " + commandOutput); 179 return commandOutput != null ? commandOutput.trim() : ""; 180 } 181 182 private int createAndStartUser(String extraParam) throws Exception { 183 String command = "pm create-user" + extraParam + " TestUser_" + System.currentTimeMillis(); 184 String commandOutput = executeShellCommand(command); 185 186 String[] tokens = commandOutput.split("\\s+"); 187 assertTrue(tokens.length > 0); 188 assertEquals("Success:", tokens[0]); 189 int userId = Integer.parseInt(tokens[tokens.length-1]); 190 191 // Start user for MediaSessionService to notice the created user. 192 getDevice().startUser(userId); 193 return userId; 194 } 195 196 /** 197 * Creates and starts a new user. 198 */ 199 protected int createAndStartUser() throws Exception { 200 return createAndStartUser(""); 201 } 202 203 /** 204 * Creates and starts a restricted profile for the {@param parentUserId}. 205 * 206 * @param parentUserId parent user id. 207 */ 208 protected int createAndStartRestrictedProfile(int parentUserId) throws Exception { 209 return createAndStartUser(" --profileOf " + parentUserId + " --restricted"); 210 } 211 212 /** 213 * Creates and starts a managed profile for the {@param parentUserId}. 214 * 215 * @param parentUserId parent user id. 216 */ 217 protected int createAndStartManagedProfile(int parentUserId) throws Exception { 218 return createAndStartUser(" --profileOf " + parentUserId + " --managed"); 219 } 220 221 /** 222 * Removes the user that is created during the test. 223 * <p>It will be no-op if the user cannot be removed or doesn't exist. 224 * 225 * @param userId user ID to remove. 226 */ 227 protected void removeUser(int userId) throws Exception { 228 if (getDevice().listUsers().contains(userId) && userId != USER_SYSTEM 229 && !mExistingUsers.contains(userId)) { 230 // Don't log output, as tests sometimes set no debug user restriction, which 231 // causes this to fail, we should still continue and remove the user. 232 String stopUserCommand = "am stop-user -w -f " + userId; 233 CLog.d("Stopping and removing user " + userId); 234 getDevice().executeShellCommand(stopUserCommand); 235 assertTrue("Couldn't remove user", getDevice().removeUser(userId)); 236 } 237 } 238 239 /** 240 * Runs tests on the device as if it's {@param userId}. 241 * 242 * @param pkgName test package file name that contains the {@link AndroidTestCase} 243 * @param testClassName Class name to test within the test package. Can be {@code null} if you 244 * want to run all test classes in the package. 245 * @param testMethodName Method name to test within the test class. Can be {@code null} if you 246 * want to run all test methods in the class. Will be ignored if {@param testClassName} is 247 * {@code null}. 248 * @param userId user ID to run the tests as. 249 */ 250 protected void runDeviceTestsAsUser( 251 String pkgName, @Nullable String testClassName, 252 @Nullable String testMethodName, int userId) throws DeviceNotAvailableException { 253 if (testClassName != null && testClassName.startsWith(".")) { 254 testClassName = pkgName + testClassName; 255 } 256 257 RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner( 258 pkgName, RUNNER, getDevice().getIDevice()); 259 testRunner.setMaxTimeToOutputResponse(DEFAULT_SHELL_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 260 testRunner.addInstrumentationArg( 261 TEST_TIMEOUT_INST_ARGS_KEY, Long.toString(DEFAULT_TEST_TIMEOUT_MILLIS)); 262 if (testClassName != null && testMethodName != null) { 263 testRunner.setMethodName(testClassName, testMethodName); 264 } else if (testClassName != null) { 265 testRunner.setClassName(testClassName); 266 } 267 268 CollectingTestListener listener = new CollectingTestListener(); 269 assertTrue(getDevice().runInstrumentationTestsAsUser(testRunner, userId, listener)); 270 271 final TestRunResult result = listener.getCurrentRunResults(); 272 if (result.isRunFailure()) { 273 throw new AssertionError("Failed to successfully run device tests for " 274 + result.getName() + ": " + result.getRunFailureMessage()); 275 } 276 if (result.getNumTests() == 0) { 277 throw new AssertionError("No tests were run on the device"); 278 } 279 280 if (result.hasFailedTests()) { 281 // Build a meaningful error message 282 StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n"); 283 for (Map.Entry<TestDescription, TestResult> resultEntry : 284 result.getTestResults().entrySet()) { 285 if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) { 286 errorBuilder.append(resultEntry.getKey().toString()); 287 errorBuilder.append(":\n"); 288 errorBuilder.append(resultEntry.getValue().getStackTrace()); 289 } 290 } 291 throw new AssertionError(errorBuilder.toString()); 292 } 293 } 294 295 /** 296 * Checks whether it is possible to create the desired number of users. 297 */ 298 protected boolean canCreateAdditionalUsers(int numberOfUsers) 299 throws DeviceNotAvailableException { 300 return getDevice().listUsers().size() + numberOfUsers <= 301 getDevice().getMaxNumberOfUsersSupported(); 302 } 303 304 /** 305 * Gets the system setting as a string from the system settings provider for the user. 306 * 307 * @param namespace namespace of the setting. 308 * @param name name of the setting. 309 * @param userId user ID to query the setting. Can be {@link #USER_ALL}. 310 * @return value of the system setting provider with the given namespace and name. 311 * {@code null}, empty string, or "null" will be returned to the empty string ("") instead. 312 */ 313 protected @Nonnull String getSettings(@Nonnull String namespace, @Nonnull String name, 314 int userId) throws Exception { 315 String userFlag = (userId == USER_ALL) ? "" : " --user " + userId; 316 String commandOutput = executeShellCommand( 317 "settings" + userFlag + " get " + namespace + " " + name); 318 if (commandOutput == null || commandOutput.isEmpty() || commandOutput.equals("null")) { 319 commandOutput = ""; 320 } 321 return commandOutput; 322 } 323 324 /** 325 * Puts the string to the system settings provider for the user. 326 * <p>This deletes the setting for an empty {@param value} as 'settings put' doesn't allow 327 * putting empty value. 328 * 329 * @param namespace namespace of the setting. 330 * @param name name of the setting. 331 * @param value value of the system setting provider with the given namespace and name. 332 * @param userId user ID to set the setting. Can be {@link #USER_ALL}. 333 */ 334 protected void putSettings(@Nonnull String namespace, @Nonnull String name, 335 @Nullable String value, int userId) throws Exception { 336 if (value == null || value.isEmpty()) { 337 // Delete the setting if the value is null or empty as 'settings put' doesn't accept 338 // them. 339 // Ignore userId here because 'settings delete' doesn't support it. 340 executeShellCommand("settings delete " + namespace + " " + name); 341 } else { 342 String userFlag = (userId == USER_ALL) ? "" : " --user " + userId; 343 executeShellCommand("settings" + userFlag + " put " + namespace + " " + name 344 + " " + value); 345 } 346 } 347 348 protected boolean hasDeviceFeature(String requiredFeature) throws DeviceNotAvailableException { 349 if (mAvailableFeatures == null) { 350 // TODO: Move this logic to ITestDevice. 351 String command = "pm list features"; 352 String commandOutput = getDevice().executeShellCommand(command); 353 CLog.i("Output for command " + command + ": " + commandOutput); 354 355 // Extract the id of the new user. 356 mAvailableFeatures = new HashSet<>(); 357 for (String feature : commandOutput.split("\\s+")) { 358 // Each line in the output of the command has the format "feature:{FEATURE_VALUE}". 359 String[] tokens = feature.split(":"); 360 assertTrue( 361 "\"" + feature + "\" expected to have format feature:{FEATURE_VALUE}", 362 tokens.length > 1); 363 assertEquals(feature, "feature", tokens[0]); 364 mAvailableFeatures.add(tokens[1]); 365 } 366 } 367 boolean result = mAvailableFeatures.contains(requiredFeature); 368 return result; 369 } 370 } 371