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