1 /*
2  * Copyright (C) 2023 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.transparency.test;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.platform.test.annotations.LargeTest;
26 import android.platform.test.annotations.Presubmit;
27 
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.log.LogUtil.CLog;
30 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
31 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
32 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
33 import com.android.tradefed.util.CommandResult;
34 import com.android.tradefed.util.CommandStatus;
35 
36 import org.junit.Before;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 
40 import java.util.concurrent.TimeUnit;
41 
42 @Presubmit
43 @RunWith(DeviceJUnit4ClassRunner.class)
44 public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test {
45     private static final String PACKAGE_NAME = "android.transparency.test.app";
46 
47     private static final String JOB_ID = "1740526926";
48 
49     /** Waiting time for the job to be scheduled */
50     private static final int JOB_CREATION_MAX_SECONDS = 30;
51 
52     @Before
setUp()53     public void setUp() throws Exception {
54         cancelPendingJob();
55     }
56 
57     @Test
testCollectAllApexInfo()58     public void testCollectAllApexInfo() throws Exception {
59         var options = new DeviceTestRunOptions(PACKAGE_NAME);
60         options.setTestClassName(PACKAGE_NAME + ".BinaryTransparencyTest");
61         options.setTestMethodName("testCollectAllApexInfo");
62 
63         // Collect APEX package names from /apex, then pass them as expectation to be verified.
64         // The package names are collected from the find name with deduplication (NB: we used to
65         // deduplicate by dropping directory names with '@', but there's a DCLA case where it only
66         // has one directory with '@'. So we have to keep it and deduplicate the current way).
67         CommandResult result = getDevice().executeShellV2Command(
68                 "ls -d /apex/*/ |grep -v /apex/sharedlibs |cut -d/ -f3 |cut -d@ -f1 |sort |uniq");
69         assertTrue(result.getStatus() == CommandStatus.SUCCESS);
70         String[] packageNames = result.getStdout().split("\n");
71         for (var i = 0; i < packageNames.length; i++) {
72             options.addInstrumentationArg("apex-" + String.valueOf(i), packageNames[i]);
73         }
74         options.addInstrumentationArg("apex-number", Integer.toString(packageNames.length));
75         runDeviceTests(options);
76     }
77 
78     @Test
testCollectAllUpdatedPreloadInfo()79     public void testCollectAllUpdatedPreloadInfo() throws Exception {
80         try {
81             updatePreloadApp();
82             runDeviceTest("testCollectAllUpdatedPreloadInfo");
83         } finally {
84             // No need to wait until job complete, since we can't verifying very meaningfully.
85             cancelPendingJob();
86             uninstallPackage("com.android.egg");
87         }
88     }
89 
90     @Test
testCollectAllSilentInstalledMbaInfo()91     public void testCollectAllSilentInstalledMbaInfo() throws Exception {
92         try {
93             new InstallMultiple()
94                 .addFile("ApkVerityTestApp.apk")
95                 .addFile("ApkVerityTestAppSplit.apk")
96                 .run();
97             updatePreloadApp();
98             assertNotNull(getDevice().getAppPackageInfo("com.android.apkverity"));
99             assertNotNull(getDevice().getAppPackageInfo("com.android.egg"));
100 
101             assertTrue(getDevice().setProperty("debug.transparency.bg-install-apps",
102                         "com.android.apkverity,com.android.egg"));
103             runDeviceTest("testCollectAllSilentInstalledMbaInfo");
104         } finally {
105             // No need to wait until job complete, since we can't verifying very meaningfully.
106             cancelPendingJob();
107             uninstallPackage("com.android.apkverity");
108             uninstallPackage("com.android.egg");
109         }
110     }
111 
112     @LargeTest
113     @Test
testRebootlessApexUpdateTriggersJobScheduling()114     public void testRebootlessApexUpdateTriggersJobScheduling() throws Exception {
115         try {
116             installRebootlessApex();
117 
118             // Verify
119             expectJobToBeScheduled();
120         } finally {
121             // No need to wait until job complete, since we can't verifying very meaningfully.
122             uninstallRebootlessApexThenReboot();
123         }
124     }
125 
126     @Test
testPreloadUpdateTriggersJobScheduling()127     public void testPreloadUpdateTriggersJobScheduling() throws Exception {
128         try {
129             updatePreloadApp();
130 
131             // Verify
132             expectJobToBeScheduled();
133         } finally {
134             // No need to wait until job complete, since we can't verifying very meaningfully.
135             cancelPendingJob();
136             uninstallPackage("com.android.egg");
137         }
138     }
139 
runDeviceTest(String method)140     private void runDeviceTest(String method) throws DeviceNotAvailableException {
141         var options = new DeviceTestRunOptions(PACKAGE_NAME);
142         options.setTestClassName(PACKAGE_NAME + ".BinaryTransparencyTest");
143         options.setTestMethodName(method);
144         runDeviceTests(options);
145     }
146 
cancelPendingJob()147     private void cancelPendingJob() throws DeviceNotAvailableException {
148         CommandResult result = getDevice().executeShellV2Command(
149                 "cmd jobscheduler cancel android " + JOB_ID);
150         if (result.getStatus() == CommandStatus.SUCCESS) {
151             CLog.d("Canceling, output: " + result.getStdout());
152         } else {
153             CLog.d("Something went wrong, error: " + result.getStderr());
154         }
155     }
156 
expectJobToBeScheduled()157     private void expectJobToBeScheduled() throws Exception {
158         for (int i = 0; i < JOB_CREATION_MAX_SECONDS; i++) {
159             CommandResult result = getDevice().executeShellV2Command(
160                     "cmd jobscheduler get-job-state android " + JOB_ID);
161             String state = result.getStdout().toString();
162             CLog.i("Job status: " + state);
163             if (state.startsWith("unknown")) {
164                 // The job hasn't been scheduled yet. So try again.
165                 TimeUnit.SECONDS.sleep(1);
166             } else if (result.getExitCode() != 0) {
167                 fail("Failing due to unexpected job state: " + result);
168             } else {
169                 // The job exists, which is all we care about here
170                 return;
171             }
172         }
173         fail("Timed out waiting for the job to be scheduled");
174     }
175 
installRebootlessApex()176     private void installRebootlessApex() throws Exception {
177         installPackage("com.android.apex.cts.shim.v2_rebootless.apex", "--force-non-staged");
178     }
179 
uninstallRebootlessApexThenReboot()180     private void uninstallRebootlessApexThenReboot() throws DeviceNotAvailableException {
181         // Reboot only if the APEX is not the pre-install one.
182         CommandResult result = getDevice().executeShellV2Command(
183                 "pm list packages -f --apex-only |grep com.android.apex.cts.shim");
184         assertTrue(result.getStatus() == CommandStatus.SUCCESS);
185         if (result.getStdout().contains("/data/apex/active/")) {
186             uninstallPackage("com.android.apex.cts.shim");
187             getDevice().reboot();
188 
189             // Reboot enforces SELinux. Make it permissive again.
190             CommandResult runResult = getDevice().executeShellV2Command("setenforce 0");
191             assertTrue(runResult.getStatus() == CommandStatus.SUCCESS);
192         }
193     }
194 
updatePreloadApp()195     private void updatePreloadApp() throws DeviceNotAvailableException {
196         CommandResult result = getDevice().executeShellV2Command("pm path com.android.egg");
197         assertTrue(result.getStatus() == CommandStatus.SUCCESS);
198         assertThat(result.getStdout()).startsWith("package:/system/app/");
199         String path = result.getStdout().replaceFirst("^package:", "");
200 
201         result = getDevice().executeShellV2Command("pm install " + path);
202         assertTrue(result.getStatus() == CommandStatus.SUCCESS);
203     }
204 
205     private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
InstallMultiple()206         InstallMultiple() {
207             super(getDevice(), getBuild());
208             // Needed since in getMockBackgroundInstalledPackages, getPackageInfo runs as the caller
209             // uid. This also makes it consistent with installPackage's behavior.
210             addArg("--force-queryable");
211         }
212     }
213 }
214