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.sdkext.extensions;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNull;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assume.assumeTrue;
26 
27 import android.cts.install.lib.host.InstallUtilsHost;
28 
29 import com.android.modules.utils.build.testing.DeviceSdkLevel;
30 import com.android.os.ext.testing.CurrentVersion;
31 import com.android.tests.rollback.host.AbandonSessionsRule;
32 import com.android.tradefed.device.ITestDevice.ApexInfo;
33 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
34 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
35 import com.android.tradefed.util.CommandResult;
36 
37 import org.junit.After;
38 import org.junit.Before;
39 import org.junit.Rule;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 
43 import java.io.File;
44 import java.time.Duration;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 
48 @RunWith(DeviceJUnit4ClassRunner.class)
49 public class SdkExtensionsHostTest extends BaseHostJUnit4Test {
50 
51     private static final String APP_FILENAME = "sdkextensions_e2e_test_app.apk";
52     private static final String APP_PACKAGE = "com.android.sdkext.extensions.apps";
53     private static final String MEDIA_FILENAME = "test_com.android.media.apex";
54     private static final String SDKEXTENSIONS_FILENAME = "test_com.android.sdkext.apex";
55 
appFilename(String appName)56     private static String appFilename(String appName) {
57         return "sdkextensions_e2e_test_app_req_" + appName + ".apk";
58     }
59 
appPackage(String appName)60     private static String appPackage(String appName) {
61         return "com.android.sdkext.extensions.apps." + appName;
62     }
63 
64     private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
65 
66     private final InstallUtilsHost mInstallUtils = new InstallUtilsHost(this);
67 
68     private DeviceSdkLevel mDeviceSdkLevel;
69     private Boolean mIsAtLeastS = null;
70     private Boolean mIsAtLeastT = null;
71     private Boolean mIsAtLeastU = null;
72 
73     @Rule public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
74 
75     @Before
setUp()76     public void setUp() throws Exception {
77         assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
78         mDeviceSdkLevel = new DeviceSdkLevel(getDevice());
79     }
80 
81     @Before
installTestApp()82     public void installTestApp() throws Exception {
83         File testAppFile = mInstallUtils.getTestFile(APP_FILENAME);
84         String installResult = getDevice().installPackage(testAppFile, true);
85         assertNull(installResult);
86     }
87 
88     @Before // Generally not needed, but local test devices are sometimes in a "bad" start state.
89     @After
cleanup()90     public void cleanup() throws Exception {
91         getDevice().uninstallPackage(APP_PACKAGE);
92         uninstallApexes(SDKEXTENSIONS_FILENAME, MEDIA_FILENAME);
93     }
94 
95     @Test
testDefault()96     public void testDefault() throws Exception {
97         assertVersionDefault();
98     }
99 
100     @Test
upgradeOneApexWithBump()101     public void upgradeOneApexWithBump() throws Exception {
102         assertVersionDefault();
103         mInstallUtils.installApexes(SDKEXTENSIONS_FILENAME);
104         reboot();
105 
106         // Version 12 requires sdkext, which is fulfilled
107         // Version 45 requires sdkext + media, which isn't fulfilled
108         assertRVersionEquals(12);
109         assertSVersionEquals(12);
110         assertTVersionEquals(12);
111         assertTestMethodsPresent(); // 45 APIs are available on 12 too.
112     }
113 
114     @Test
upgradeOneApex()115     public void upgradeOneApex() throws Exception {
116         // Version 45 requires updated sdkext and media, so updating just media changes nothing.
117         assertVersionDefault();
118         mInstallUtils.installApexes(MEDIA_FILENAME);
119         reboot();
120         assertVersionDefault();
121     }
122 
123     @Test
upgradeTwoApexes()124     public void upgradeTwoApexes() throws Exception {
125         // Updating sdkext and media bumps the version to 45.
126         assertVersionDefault();
127         mInstallUtils.installApexes(MEDIA_FILENAME, SDKEXTENSIONS_FILENAME);
128         reboot();
129         assertVersion45();
130     }
131 
canInstallApp(String appName)132     private boolean canInstallApp(String appName) throws Exception {
133         File appFile = mInstallUtils.getTestFile(appFilename(appName));
134         String installResult = getDevice().installPackage(appFile, true);
135         if (installResult != null) {
136             return false;
137         }
138         assertNull(getDevice().uninstallPackage(appPackage(appName)));
139         return true;
140     }
141 
getExtensionVersionFromSysprop(String v)142     private String getExtensionVersionFromSysprop(String v) throws Exception {
143         String command = "getprop build.version.extensions." + v;
144         CommandResult res = getDevice().executeShellV2Command(command);
145         checkExitCode(command, res);
146         return res.getStdout().replace("\n", "");
147     }
148 
broadcast(String action, String extra)149     private String broadcast(String action, String extra) throws Exception {
150         String command = getBroadcastCommand(action, extra);
151         CommandResult res = getDevice().executeShellV2Command(command);
152         checkExitCode(command, res);
153         Matcher matcher = Pattern.compile("data=\"([^\"]+)\"").matcher(res.getStdout());
154         assertTrue("Unexpected output from am broadcast: " + res.getStdout(), matcher.find());
155         return matcher.group(1);
156     }
157 
checkExitCode(String command, CommandResult res)158     private static void checkExitCode(String command, CommandResult res) {
159         int exitCode = (int) res.getExitCode();
160         if (exitCode != 0) {
161             throw new IllegalStateException(
162                     String.format(
163                             "Unexpected result from `%s`\n"
164                                     + "    exitCode=%d\n"
165                                     + "    stderr=\n"
166                                     + "%s\n"
167                                     + "    stdout=\n"
168                                     + "%s\n",
169                             command, exitCode, res.getStderr(), res.getStdout()));
170         }
171     }
172 
broadcastForBoolean(String action, String extra)173     private boolean broadcastForBoolean(String action, String extra) throws Exception {
174         String result = broadcast(action, extra);
175         if (result.equals("true") || result.equals("false")) {
176             return result.equals("true");
177         }
178         throw getAppParsingError(result);
179     }
180 
broadcastForInt(String action, String extra)181     private int broadcastForInt(String action, String extra) throws Exception {
182         String result = broadcast(action, extra);
183         try {
184             return Integer.parseInt(result);
185         } catch (NumberFormatException e) {
186             throw getAppParsingError(result);
187         }
188     }
189 
getAppParsingError(String result)190     private Error getAppParsingError(String result) {
191         String message = "App error! Full stack trace in logcat (grep for SdkExtensionsE2E): ";
192         return new AssertionError(message + result);
193     }
194 
assertVersionDefault()195     private void assertVersionDefault() throws Exception {
196         int expected =
197                 isAtLeastU()
198                         ? CurrentVersion.CURRENT_TRAIN_VERSION
199                         : isAtLeastT()
200                                 ? CurrentVersion.T_BASE_VERSION
201                                 : isAtLeastS()
202                                         ? CurrentVersion.S_BASE_VERSION
203                                         : CurrentVersion.R_BASE_VERSION;
204         assertRVersionEquals(expected);
205         assertSVersionEquals(expected);
206         assertTVersionEquals(expected);
207         assertTestMethodsNotPresent();
208     }
209 
assertVersion45()210     private void assertVersion45() throws Exception {
211         assertRVersionEquals(45);
212         assertSVersionEquals(45);
213         assertTVersionEquals(45);
214         assertTestMethodsPresent();
215     }
216 
assertTestMethodsNotPresent()217     private void assertTestMethodsNotPresent() throws Exception {
218         assertTrue(broadcastForBoolean("MAKE_CALLS_DEFAULT", null));
219     }
220 
assertTestMethodsPresent()221     private void assertTestMethodsPresent() throws Exception {
222         if (isAtLeastS()) {
223             assertTrue(broadcastForBoolean("MAKE_CALLS_45", null));
224         } else {
225             // The APIs in the test apex are not currently getting installed correctly
226             // on Android R devices because they rely on the dynamic classpath feature.
227             // TODO(b/234361913): fix this
228             assertTestMethodsNotPresent();
229         }
230     }
231 
assertRVersionEquals(int version)232     private void assertRVersionEquals(int version) throws Exception {
233         String[] apps =
234                 version >= 45
235                         ? new String[] {"r12", "r45"}
236                         : version >= 12 ? new String[] {"r12"} : new String[] {};
237         assertExtensionVersionEquals("r", version, apps, true);
238     }
239 
assertSVersionEquals(int version)240     private void assertSVersionEquals(int version) throws Exception {
241         // These APKs require the same R version as they do S version.
242         int minVersion = Math.min(version, broadcastForInt("GET_SDK_VERSION", "r"));
243         String[] apps =
244                 minVersion >= 45
245                         ? new String[] {"s12", "s45"}
246                         : minVersion >= 12 ? new String[] {"s12"} : new String[] {};
247         assertExtensionVersionEquals("s", version, apps, isAtLeastS());
248     }
249 
assertTVersionEquals(int version)250     private void assertTVersionEquals(int version) throws Exception {
251         assertExtensionVersionEquals("t", version, new String[] {}, isAtLeastT());
252     }
253 
assertExtensionVersionEquals( String extension, int version, String[] apps, boolean expected)254     private void assertExtensionVersionEquals(
255             String extension, int version, String[] apps, boolean expected) throws Exception {
256         int appValue = broadcastForInt("GET_SDK_VERSION", extension);
257         String syspropValue = getExtensionVersionFromSysprop(extension);
258         if (expected) {
259             assertEquals(version, appValue);
260             assertEquals(String.valueOf(version), syspropValue);
261             for (String app : apps) {
262                 assertTrue(canInstallApp(app));
263             }
264         } else {
265             assertEquals(0, appValue);
266             assertEquals("", syspropValue);
267             for (String app : apps) {
268                 assertFalse(canInstallApp(app));
269             }
270         }
271     }
272 
getBroadcastCommand(String action, String extra)273     private static String getBroadcastCommand(String action, String extra) {
274         String cmd = "am broadcast";
275         cmd += " -a com.android.sdkext.extensions.apps." + action;
276         if (extra != null) {
277             cmd += " -e extra " + extra;
278         }
279         cmd += " -n com.android.sdkext.extensions.apps/.Receiver";
280         return cmd;
281     }
282 
isAtLeastS()283     private boolean isAtLeastS() throws Exception {
284         if (mIsAtLeastS == null) {
285             mIsAtLeastS = mDeviceSdkLevel.isDeviceAtLeastS();
286         }
287         return mIsAtLeastS;
288     }
289 
isAtLeastT()290     private boolean isAtLeastT() throws Exception {
291         if (mIsAtLeastT == null) {
292             mIsAtLeastT = mDeviceSdkLevel.isDeviceAtLeastT();
293         }
294         return mIsAtLeastT;
295     }
296 
isAtLeastU()297     private boolean isAtLeastU() throws Exception {
298         if (mIsAtLeastU == null) {
299             mIsAtLeastU = mDeviceSdkLevel.isDeviceAtLeastU();
300         }
301         return mIsAtLeastU;
302     }
303 
uninstallApexes(String... filenames)304     private boolean uninstallApexes(String... filenames) throws Exception {
305         boolean reboot = false;
306         for (String filename : filenames) {
307             ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(filename));
308             String res = getDevice().uninstallPackage(apex.name);
309             // res is null for successful uninstalls (non-null likely implesfactory version).
310             reboot |= res == null;
311         }
312         if (reboot) {
313             reboot();
314             return true;
315         }
316         return false;
317     }
318 
reboot()319     private void reboot() throws Exception {
320         getDevice().reboot();
321         boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
322         assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
323     }
324 }
325