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