1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tests.odsign; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assume.assumeTrue; 24 25 import android.cts.install.lib.host.InstallUtilsHost; 26 27 import com.android.tradefed.device.DeviceNotAvailableException; 28 import com.android.tradefed.device.TestDeviceOptions; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.util.CommandResult; 31 32 import com.google.common.io.ByteStreams; 33 34 import org.w3c.dom.Document; 35 import org.w3c.dom.Element; 36 import org.w3c.dom.NodeList; 37 38 import java.io.File; 39 import java.io.FileOutputStream; 40 import java.io.InputStream; 41 import java.io.OutputStream; 42 import java.time.Duration; 43 import java.time.ZonedDateTime; 44 import java.time.format.DateTimeFormatter; 45 import java.util.Arrays; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 import java.util.stream.Stream; 54 55 import javax.xml.parsers.DocumentBuilder; 56 import javax.xml.parsers.DocumentBuilderFactory; 57 58 public class OdsignTestUtils { 59 public static final String ART_APEX_DALVIK_CACHE_DIRNAME = 60 "/data/misc/apexdata/com.android.art/dalvik-cache"; 61 public static final String CACHE_INFO_FILE = ART_APEX_DALVIK_CACHE_DIRNAME + "/cache-info.xml"; 62 public static final String APEX_INFO_FILE = "/apex/apex-info-list.xml"; 63 64 private static final String ODREFRESH_BIN = "odrefresh"; 65 66 public static final String ZYGOTE_32_NAME = "zygote"; 67 public static final String ZYGOTE_64_NAME = "zygote64"; 68 69 public static final List<String> APP_ARTIFACT_EXTENSIONS = List.of(".art", ".odex", ".vdex"); 70 public static final List<String> BCP_ARTIFACT_EXTENSIONS = List.of(".art", ".oat", ".vdex"); 71 72 private static final String ODREFRESH_COMPILATION_LOG = 73 "/data/misc/odrefresh/compilation-log.txt"; 74 75 private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(5); 76 private static final Duration RESTART_ZYGOTE_COMPLETE_TIMEOUT = Duration.ofMinutes(3); 77 78 private static final String TAG = "OdsignTestUtils"; 79 private static final String PACKAGE_NAME_KEY = TAG + ":PACKAGE_NAME"; 80 private static final String VERITY_DISABLED_BY_TEST_KEY = TAG + ":VERITY_DISABLED_BY_TEST"; 81 82 // Keep in sync with `ABI_TO_INSTRUCTION_SET_MAP` in 83 // libcore/libart/src/main/java/dalvik/system/VMRuntime.java. 84 private static final Map<String, String> ABI_TO_INSTRUCTION_SET_MAP = 85 Map.of("armeabi", "arm", "armeabi-v7a", "arm", "x86", "x86", "x86_64", "x86_64", 86 "arm64-v8a", "arm64", "arm64-v8a-hwasan", "arm64", "riscv64", "riscv64"); 87 88 private final InstallUtilsHost mInstallUtils; 89 private final TestInformation mTestInfo; 90 OdsignTestUtils(TestInformation testInfo)91 public OdsignTestUtils(TestInformation testInfo) throws Exception { 92 assertThat(testInfo.getDevice()).isNotNull(); 93 mInstallUtils = new InstallUtilsHost(testInfo); 94 mTestInfo = testInfo; 95 } 96 97 /** 98 * Re-installs the current active ART module on device. 99 */ installTestApex()100 public void installTestApex() throws Exception { 101 assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported()); 102 103 String packagesOutput = 104 mTestInfo.getDevice().executeShellCommand("pm list packages -f --apex-only"); 105 Pattern p = Pattern.compile( 106 "^package:(.*)=(com(?:\\.google)?\\.android(?:\\.go)?\\.art)$", Pattern.MULTILINE); 107 Matcher m = p.matcher(packagesOutput); 108 assertTrue("ART module not found. Packages are:\n" + packagesOutput, m.find()); 109 String artApexPath = m.group(1); 110 String artApexName = m.group(2); 111 112 assertCommandSucceeds("pm install --apex " + artApexPath); 113 114 mTestInfo.properties().put(PACKAGE_NAME_KEY, artApexName); 115 116 removeCompilationLogToAvoidBackoff(); 117 } 118 uninstallTestApex()119 public void uninstallTestApex() throws Exception { 120 String packageName = mTestInfo.properties().get(PACKAGE_NAME_KEY); 121 if (packageName != null) { 122 mTestInfo.getDevice().uninstallPackage(packageName); 123 removeCompilationLogToAvoidBackoff(); 124 } 125 } 126 getMappedArtifacts(String pid, String grepPattern)127 public Set<String> getMappedArtifacts(String pid, String grepPattern) throws Exception { 128 String grepCommand = String.format("grep \"%s\" /proc/%s/maps", grepPattern, pid); 129 Set<String> mappedFiles = new HashSet<>(); 130 for (String line : assertCommandSucceeds(grepCommand).split("\\R")) { 131 int start = line.indexOf(ART_APEX_DALVIK_CACHE_DIRNAME); 132 if (line.contains("[") || line.contains("(deleted)")) { 133 // Ignore anonymously mapped sections, which are quoted in square braces, and 134 // deleted mapped files. 135 continue; 136 } 137 mappedFiles.add(line.substring(start)); 138 } 139 return mappedFiles; 140 } 141 142 /** 143 * Returns the mapped artifacts of the Zygote process. 144 */ getZygoteLoadedArtifacts(String zygoteName)145 public Set<String> getZygoteLoadedArtifacts(String zygoteName) throws Exception { 146 // There may be multiple Zygote processes when Zygote just forks and has not executed any 147 // app binary. We can take any of the pids. 148 // We can't use the "-s" flag when calling `pidof` because the Toybox's `pidof` 149 // implementation is wrong and it outputs multiple pids regardless of the "-s" flag, so we 150 // split the output and take the first pid ourselves. 151 String zygotePid = assertCommandSucceeds("pidof " + zygoteName).split("\\s+")[0]; 152 assertTrue(!zygotePid.isEmpty()); 153 154 String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + "/.*/boot"; 155 return getMappedArtifacts(zygotePid, grepPattern); 156 } 157 getSystemServerLoadedArtifacts()158 public Set<String> getSystemServerLoadedArtifacts() throws Exception { 159 String systemServerPid = assertCommandSucceeds("pidof system_server"); 160 assertTrue(!systemServerPid.isEmpty()); 161 assertTrue("There should be exactly one `system_server` process", 162 systemServerPid.matches("\\d+")); 163 164 // system_server artifacts are in the APEX data dalvik cache and names all contain 165 // the word "@classes". Look for mapped files that match this pattern in the proc map for 166 // system_server. 167 String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + "/.*@classes"; 168 return getMappedArtifacts(systemServerPid, grepPattern); 169 } 170 getExpectedBootImage(String bootImageStem, String isa)171 private Set<String> getExpectedBootImage(String bootImageStem, String isa) throws Exception { 172 Set<String> artifacts = new HashSet<>(); 173 for (String extension : BCP_ARTIFACT_EXTENSIONS) { 174 artifacts.add(String.format( 175 "%s/%s/%s%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa, bootImageStem, extension)); 176 } 177 return artifacts; 178 } 179 getExpectedBootImage(String bootImageStem)180 private Set<String> getExpectedBootImage(String bootImageStem) throws Exception { 181 Set<String> artifacts = new HashSet<>(); 182 for (String isa : getZygoteNamesAndIsas().values()) { 183 artifacts.addAll(getExpectedBootImage(bootImageStem, isa)); 184 } 185 return artifacts; 186 } 187 getExpectedPrimaryBootImage()188 public Set<String> getExpectedPrimaryBootImage() throws Exception { 189 return getExpectedBootImage("boot"); 190 } 191 getExpectedMinimalBootImage()192 public Set<String> getExpectedMinimalBootImage() throws Exception { 193 return getExpectedBootImage("boot_minimal"); 194 } 195 getExpectedBootImageMainlineExtension()196 public Set<String> getExpectedBootImageMainlineExtension() throws Exception { 197 return getExpectedBootImage("boot-" + getFirstMainlineFrameworkLibraryName()); 198 } 199 getSystemServerExpectedArtifacts()200 public Set<String> getSystemServerExpectedArtifacts() throws Exception { 201 String[] classpathElements = getListFromEnvironmentVariable("SYSTEMSERVERCLASSPATH"); 202 assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0); 203 String[] standaloneJars = getListFromEnvironmentVariable("STANDALONE_SYSTEMSERVER_JARS"); 204 String[] allSystemServerJars = 205 Stream.concat(Arrays.stream(classpathElements), Arrays.stream(standaloneJars)) 206 .toArray(String[] ::new); 207 String isa = getSystemServerIsa(); 208 209 Set<String> artifacts = new HashSet<>(); 210 for (String jar : allSystemServerJars) { 211 artifacts.addAll(getApexDataDalvikCacheFilenames(jar, isa)); 212 } 213 214 return artifacts; 215 } 216 217 // Verifies that boot image files with the given stem are loaded by Zygote for each instruction 218 // set. verifyZygotesLoadedBootImage(String bootImageStem)219 private void verifyZygotesLoadedBootImage(String bootImageStem) throws Exception { 220 for (var entry : getZygoteNamesAndIsas().entrySet()) { 221 assertThat(getZygoteLoadedArtifacts(entry.getKey())) 222 .containsAtLeastElementsIn( 223 getExpectedBootImage(bootImageStem, entry.getValue())); 224 } 225 } 226 verifyZygotesLoadedPrimaryBootImage()227 public void verifyZygotesLoadedPrimaryBootImage() throws Exception { 228 verifyZygotesLoadedBootImage("boot"); 229 } 230 verifyZygotesLoadedMinimalBootImage()231 public void verifyZygotesLoadedMinimalBootImage() throws Exception { 232 verifyZygotesLoadedBootImage("boot_minimal"); 233 } 234 verifyZygotesLoadedBootImageMainlineExtension()235 public void verifyZygotesLoadedBootImageMainlineExtension() throws Exception { 236 verifyZygotesLoadedBootImage("boot-" + getFirstMainlineFrameworkLibraryName()); 237 } 238 verifySystemServerLoadedArtifacts(Set<String> expectedArtifacts)239 public void verifySystemServerLoadedArtifacts(Set<String> expectedArtifacts) throws Exception { 240 assertThat(getSystemServerLoadedArtifacts()) 241 .containsAtLeastElementsIn(expectedArtifacts); 242 } 243 verifySystemServerLoadedArtifacts()244 public void verifySystemServerLoadedArtifacts() throws Exception { 245 verifySystemServerLoadedArtifacts(getSystemServerExpectedArtifacts()); 246 } 247 haveCompilationLog()248 public boolean haveCompilationLog() throws Exception { 249 CommandResult result = 250 mTestInfo.getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG); 251 return result.getExitCode() == 0; 252 } 253 removeCompilationLogToAvoidBackoff()254 public void removeCompilationLogToAvoidBackoff() throws Exception { 255 mTestInfo.getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG); 256 } 257 reboot()258 public void reboot() throws Exception { 259 TestDeviceOptions options = mTestInfo.getDevice().getOptions(); 260 // store default value and increase time-out for reboot 261 int rebootTimeout = options.getRebootTimeout(); 262 long onlineTimeout = options.getOnlineTimeout(); 263 options.setRebootTimeout((int) BOOT_COMPLETE_TIMEOUT.toMillis()); 264 options.setOnlineTimeout(BOOT_COMPLETE_TIMEOUT.toMillis()); 265 mTestInfo.getDevice().setOptions(options); 266 267 mTestInfo.getDevice().reboot(); 268 boolean success = 269 mTestInfo.getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis()); 270 271 // restore default values 272 options.setRebootTimeout(rebootTimeout); 273 options.setOnlineTimeout(onlineTimeout); 274 mTestInfo.getDevice().setOptions(options); 275 276 assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue(); 277 } 278 restartZygote()279 public void restartZygote() throws Exception { 280 // `waitForBootComplete` relies on `dev.bootcomplete`. 281 mTestInfo.getDevice().executeShellCommand("setprop dev.bootcomplete 0"); 282 mTestInfo.getDevice().executeShellCommand("setprop ctl.restart zygote"); 283 boolean success = mTestInfo.getDevice().waitForBootComplete( 284 RESTART_ZYGOTE_COMPLETE_TIMEOUT.toMillis()); 285 assertWithMessage("Zygote didn't start in %s", BOOT_COMPLETE_TIMEOUT) 286 .that(success) 287 .isTrue(); 288 } 289 290 /** 291 * Returns the value of a boolean test property, or false if it does not exist. 292 */ getBooleanOrDefault(String key)293 private boolean getBooleanOrDefault(String key) { 294 String value = mTestInfo.properties().get(key); 295 if (value == null) { 296 return false; 297 } 298 return Boolean.parseBoolean(value); 299 } 300 setBoolean(String key, boolean value)301 private void setBoolean(String key, boolean value) { 302 mTestInfo.properties().put(key, Boolean.toString(value)); 303 } 304 getListFromEnvironmentVariable(String name)305 private String[] getListFromEnvironmentVariable(String name) throws Exception { 306 String systemServerClasspath = 307 mTestInfo.getDevice().executeShellCommand("echo $" + name).trim(); 308 if (!systemServerClasspath.isEmpty()) { 309 return systemServerClasspath.split(":"); 310 } 311 return new String[0]; 312 } 313 getInstructionSet(String abi)314 private static String getInstructionSet(String abi) { 315 String instructionSet = ABI_TO_INSTRUCTION_SET_MAP.get(abi); 316 assertThat(instructionSet).isNotNull(); 317 return instructionSet; 318 } 319 getZygoteNamesAndIsas()320 public Map<String, String> getZygoteNamesAndIsas() throws Exception { 321 Map<String, String> namesAndIsas = new HashMap<>(); 322 String abiList64 = mTestInfo.getDevice().getProperty("ro.product.cpu.abilist64"); 323 if (abiList64 != null && !abiList64.isEmpty()) { 324 namesAndIsas.put(ZYGOTE_64_NAME, getInstructionSet(abiList64.split(",")[0])); 325 } 326 String abiList32 = mTestInfo.getDevice().getProperty("ro.product.cpu.abilist32"); 327 if (abiList32 != null && !abiList32.isEmpty()) { 328 namesAndIsas.put(ZYGOTE_32_NAME, getInstructionSet(abiList32.split(",")[0])); 329 } 330 return namesAndIsas; 331 } 332 getSystemServerIsa()333 public String getSystemServerIsa() throws Exception { 334 return getInstructionSet( 335 mTestInfo.getDevice().getProperty("ro.product.cpu.abilist").split(",")[0]); 336 } 337 338 // Keep in sync with `GetApexDataDalvikCacheFilename` in art/libartbase/base/file_utils.cc. getApexDataDalvikCacheFilenames(String dexLocation, String isa)339 public static Set<String> getApexDataDalvikCacheFilenames(String dexLocation, String isa) 340 throws Exception { 341 Set<String> filenames = new HashSet<>(); 342 String escapedPath = dexLocation.substring(1).replace('/', '@'); 343 for (String extension : APP_ARTIFACT_EXTENSIONS) { 344 filenames.add(String.format("%s/%s/%s@classes%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa, 345 escapedPath, extension)); 346 } 347 return filenames; 348 } 349 350 // Keep in sync with `GetFirstMainlineFrameworkLibraryName` in 351 // art/libartbase/base/file_utils.cc. getFirstMainlineFrameworkLibraryName()352 private String getFirstMainlineFrameworkLibraryName() throws Exception { 353 String[] bcpElements = getListFromEnvironmentVariable("BOOTCLASSPATH"); 354 assertTrue("BOOTCLASSPATH is empty", bcpElements.length > 0); 355 String[] dex2oatBcpElements = getListFromEnvironmentVariable("DEX2OATBOOTCLASSPATH"); 356 assertTrue("DEX2OATBOOTCLASSPATH is empty", dex2oatBcpElements.length > 0); 357 assertTrue("DEX2OATBOOTCLASSPATH must be a prefix of BOOTCLASSPATH", 358 bcpElements.length > dex2oatBcpElements.length 359 && Arrays.equals( 360 Arrays.copyOfRange(bcpElements, 0, dex2oatBcpElements.length), 361 dex2oatBcpElements)); 362 363 String filename = bcpElements[dex2oatBcpElements.length]; 364 String basename = basename(filename); 365 return replaceExtension(basename, ""); 366 } 367 parseFormattedDateTime(String dateTimeStr)368 private long parseFormattedDateTime(String dateTimeStr) throws Exception { 369 DateTimeFormatter formatter = 370 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn Z"); 371 ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter); 372 return zonedDateTime.toInstant().toEpochMilli(); 373 } 374 getModifiedTimeMs(String filename)375 public long getModifiedTimeMs(String filename) throws Exception { 376 // We can't use the "-c '%.3Y'" flag when to get the timestamp because the Toybox's `stat` 377 // implementation truncates the timestamp to seconds, which is not accurate enough, so we 378 // use "-c '%%y'" and parse the time ourselves. 379 String dateTimeStr = assertCommandSucceeds(String.format("stat -c '%%y' '%s'", filename)); 380 return parseFormattedDateTime(dateTimeStr); 381 } 382 getCurrentTimeMs()383 public long getCurrentTimeMs() throws Exception { 384 // We can't use getDevice().getDeviceDate() because it truncates the timestamp to seconds, 385 // which is not accurate enough. 386 String dateTimeStr = assertCommandSucceeds("date +'%Y-%m-%d %H:%M:%S.%N %z'"); 387 return parseFormattedDateTime(dateTimeStr); 388 } 389 countFilesCreatedBeforeTime(String directory, long timestampMs)390 public int countFilesCreatedBeforeTime(String directory, long timestampMs) 391 throws DeviceNotAvailableException { 392 // Drop the precision to second, mainly because we need to use `find -newerct` to query 393 // files by timestamp, but toybox can't parse `date +'%s.%N'` currently. 394 String timestamp = String.valueOf(timestampMs / 1000); 395 // For simplicity, directory must be a simple path that doesn't require escaping. 396 String output = assertCommandSucceeds( 397 "find " + directory + " -type f ! -newerct '@" + timestamp + "' | wc -l"); 398 return Integer.parseInt(output); 399 } 400 countFilesCreatedAfterTime(String directory, long timestampMs)401 public int countFilesCreatedAfterTime(String directory, long timestampMs) 402 throws DeviceNotAvailableException { 403 // Drop the precision to second, mainly because we need to use `find -newerct` to query 404 // files by timestamp, but toybox can't parse `date +'%s.%N'` currently. 405 String timestamp = String.valueOf(timestampMs / 1000); 406 // For simplicity, directory must be a simple path that doesn't require escaping. 407 String output = assertCommandSucceeds( 408 "find " + directory + " -type f -newerct '@" + timestamp + "' | wc -l"); 409 return Integer.parseInt(output); 410 } 411 assertCommandSucceeds(String command)412 public String assertCommandSucceeds(String command) throws DeviceNotAvailableException { 413 CommandResult result = mTestInfo.getDevice().executeShellV2Command(command); 414 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 415 return result.getStdout().trim(); 416 } 417 copyResourceToFile(String resourceName)418 public File copyResourceToFile(String resourceName) throws Exception { 419 File file = File.createTempFile("odsign_e2e_tests", ".tmp"); 420 file.deleteOnExit(); 421 try (OutputStream outputStream = new FileOutputStream(file); 422 InputStream inputStream = getClass().getResourceAsStream(resourceName)) { 423 assertThat(ByteStreams.copy(inputStream, outputStream)).isGreaterThan(0); 424 } 425 return file; 426 } 427 assertModifiedAfter(Set<String> artifacts, long timeMs)428 public void assertModifiedAfter(Set<String> artifacts, long timeMs) throws Exception { 429 for (String artifact : artifacts) { 430 long modifiedTime = getModifiedTimeMs(artifact); 431 assertTrue( 432 String.format( 433 "Artifact %s is not re-compiled. Modified time: %d, Reference time: %d", 434 artifact, modifiedTime, timeMs), 435 modifiedTime > timeMs); 436 } 437 } 438 assertNotModifiedAfter(Set<String> artifacts, long timeMs)439 public void assertNotModifiedAfter(Set<String> artifacts, long timeMs) throws Exception { 440 for (String artifact : artifacts) { 441 long modifiedTime = getModifiedTimeMs(artifact); 442 assertTrue(String.format("Artifact %s is unexpectedly re-compiled. " 443 + "Modified time: %d, Reference time: %d", 444 artifact, modifiedTime, timeMs), 445 modifiedTime < timeMs); 446 } 447 } 448 449 public void assertFilesExist(Set<String> files) throws Exception { 450 assertThat(getExistingFiles(files)).containsExactlyElementsIn(files); 451 } 452 453 public void assertFilesNotExist(Set<String> files) throws Exception { 454 assertThat(getExistingFiles(files)).isEmpty(); 455 } 456 457 private Set<String> getExistingFiles(Set<String> files) throws Exception { 458 Set<String> existingFiles = new HashSet<>(); 459 for (String file : files) { 460 if (mTestInfo.getDevice().doesFileExist(file)) { 461 existingFiles.add(file); 462 } 463 } 464 return existingFiles; 465 } 466 467 public static String replaceExtension(String filename, String extension) throws Exception { 468 int index = filename.lastIndexOf("."); 469 assertTrue("Extension not found in filename: " + filename, index != -1); 470 return filename.substring(0, index) + extension; 471 } 472 473 public static String basename(String filename) throws Exception { 474 int index = filename.lastIndexOf("/"); 475 assertTrue("Slash not found in filename: " + filename, index != -1); 476 return filename.substring(index + 1); 477 } 478 479 public void runOdrefresh() throws Exception { 480 runOdrefresh("" /* extraArgs */); 481 } 482 483 public CommandResult runOdrefresh(String extraArgs) throws Exception { 484 mTestInfo.getDevice().executeShellV2Command(ODREFRESH_BIN + " --check"); 485 return mTestInfo.getDevice().executeShellV2Command(ODREFRESH_BIN 486 + " --partial-compilation=true --no-refresh " + extraArgs + " --compile"); 487 } 488 489 /** 490 * Simulates how odsign invokes odrefresh on a device that doesn't have the security fix for 491 * CVE-2021-39689 (b/206090748). 492 */ 493 public CommandResult runOdrefreshNoPartialCompilation() throws Exception { 494 // Note that odsign doesn't call `odrefresh --check` on such a device. 495 return mTestInfo.getDevice().executeShellV2Command( 496 ODREFRESH_BIN + " --partial-compilation=false --no-refresh --compile"); 497 } 498 499 public boolean areAllApexesFactoryInstalled() throws Exception { 500 Document doc = loadXml(APEX_INFO_FILE); 501 NodeList list = doc.getElementsByTagName("apex-info"); 502 for (int i = 0; i < list.getLength(); i++) { 503 Element node = (Element) list.item(i); 504 if (node.getAttribute("isActive").equals("true") 505 && node.getAttribute("isFactory").equals("false")) { 506 return false; 507 } 508 } 509 return true; 510 } 511 512 private Document loadXml(String remoteXmlFile) throws Exception { 513 File localFile = mTestInfo.getDevice().pullFile(remoteXmlFile); 514 assertThat(localFile).isNotNull(); 515 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 516 return builder.parse(localFile); 517 } 518 519 /** Disables dm-verity if it's enabled. */ 520 public void maybeDisableVerity() throws Exception { 521 boolean disabled = 522 mTestInfo.getDevice().getProperty("ro.boot.veritymode").equals("disabled"); 523 if (!disabled) { 524 assertCommandSucceeds("disable-verity"); 525 setBoolean(VERITY_DISABLED_BY_TEST_KEY, true); 526 } 527 } 528 529 /** Enables dm-verity if it's disabled by {@link #maybeDisableVerity}. */ 530 public void maybeEnableVerity() throws Exception { 531 boolean disabledByTest = getBooleanOrDefault(VERITY_DISABLED_BY_TEST_KEY); 532 if (disabledByTest) { 533 assertCommandSucceeds("enable-verity"); 534 } 535 } 536 } 537