/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdkext.extensions; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import org.junit.Ignore; import android.cts.install.lib.host.InstallUtilsHost; import com.android.os.ext.testing.CurrentVersion; import com.android.tests.rollback.host.AbandonSessionsRule; import com.android.tradefed.device.ITestDevice.ApexInfo; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; import com.android.tradefed.util.CommandResult; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.lang.NumberFormatException; import java.time.Duration; import java.util.regex.Matcher; import java.util.regex.Pattern; @RunWith(DeviceJUnit4ClassRunner.class) public class SdkExtensionsHostTest extends BaseHostJUnit4Test { private static final String APP_FILENAME = "sdkextensions_e2e_test_app.apk"; private static final String APP_PACKAGE = "com.android.sdkext.extensions.apps"; private static final String APP_R12_FILENAME = "sdkextensions_e2e_test_app_req_r12.apk"; private static final String APP_R12_PACKAGE = "com.android.sdkext.extensions.apps.r12"; private static final String APP_S12_FILENAME = "sdkextensions_e2e_test_app_req_s12.apk"; private static final String APP_S12_PACKAGE = "com.android.sdkext.extensions.apps.s12"; private static final String APP_R45_FILENAME = "sdkextensions_e2e_test_app_req_r45.apk"; private static final String APP_R45_PACKAGE = "com.android.sdkext.extensions.apps.r45"; private static final String APP_S45_FILENAME = "sdkextensions_e2e_test_app_req_s45.apk"; private static final String APP_S45_PACKAGE = "com.android.sdkext.extensions.apps.s45"; private static final String MEDIA_FILENAME = "test_com.android.media.apex"; private static final String SDKEXTENSIONS_FILENAME = "test_com.android.sdkext.apex"; private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2); private final InstallUtilsHost mInstallUtils = new InstallUtilsHost(this); private Boolean mIsAtLeastS = null; private Boolean mIsAtLeastT = null; @Rule public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); @Before public void setUp() throws Exception { assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported()); } @Before public void installTestApp() throws Exception { File testAppFile = mInstallUtils.getTestFile(APP_FILENAME); String installResult = getDevice().installPackage(testAppFile, true); assertNull(installResult); } @Before // Generally not needed, but local test devices are sometimes in a "bad" start state. @After public void cleanup() throws Exception { getDevice().uninstallPackage(APP_PACKAGE); uninstallApexes(SDKEXTENSIONS_FILENAME, MEDIA_FILENAME); } @Test @Ignore("b/274764792") public void testDefault() throws Exception { assertVersionDefault(); } @Test @Ignore("b/274764792") public void upgradeOneApexWithBump() throws Exception { assertVersionDefault(); mInstallUtils.installApexes(SDKEXTENSIONS_FILENAME); reboot(); // Version 12 requires sdkext, which is fulfilled // Version 45 requires sdkext + media, which isn't fulfilled assertRVersionEquals(12); assertSVersionEquals(12); assertTestMethodsPresent(); // 45 APIs are available on 12 too. } @Test @Ignore("b/274764792") public void upgradeOneApex() throws Exception { // Version 45 requires updated sdkext and media, so updating just media changes nothing. assertVersionDefault(); mInstallUtils.installApexes(MEDIA_FILENAME); reboot(); assertVersionDefault(); } @Test @Ignore("b/274764792") public void upgradeTwoApexes() throws Exception { // Updating sdkext and media bumps the version to 45. assertVersionDefault(); mInstallUtils.installApexes(MEDIA_FILENAME, SDKEXTENSIONS_FILENAME); reboot(); assertVersion45(); } private boolean canInstallApp(String filename, String packageName) throws Exception { File appFile = mInstallUtils.getTestFile(filename); String installResult = getDevice().installPackage(appFile, true); if (installResult != null) { return false; } assertNull(getDevice().uninstallPackage(packageName)); return true; } private String getExtensionVersionFromSysprop(String v) throws Exception { String command = "getprop build.version.extensions." + v; CommandResult res = getDevice().executeShellV2Command(command); assertEquals(0, (int) res.getExitCode()); return res.getStdout().replace("\n", ""); } private String broadcast(String action, String extra) throws Exception { String command = getBroadcastCommand(action, extra); CommandResult res = getDevice().executeShellV2Command(command); assertEquals(0, (int) res.getExitCode()); Matcher matcher = Pattern.compile("data=\"([^\"]+)\"").matcher(res.getStdout()); assertTrue("Unexpected output from am broadcast: " + res.getStdout(), matcher.find()); return matcher.group(1); } private boolean broadcastForBoolean(String action, String extra) throws Exception { String result = broadcast(action, extra); if (result.equals("true") || result.equals("false")) { return result.equals("true"); } throw getAppParsingError(result); } private int broadcastForInt(String action, String extra) throws Exception { String result = broadcast(action, extra); try { return Integer.parseInt(result); } catch (NumberFormatException e) { throw getAppParsingError(result); } } private Error getAppParsingError(String result) { String message = "App error! Full stack trace in logcat (grep for SdkExtensionsE2E): "; return new AssertionError(message + result); } private void assertVersionDefault() throws Exception { int expected = isAtLeastT() ? CurrentVersion.T_BASE_VERSION : isAtLeastS() ? CurrentVersion.S_BASE_VERSION : CurrentVersion.R_BASE_VERSION; assertRVersionEquals(expected); assertSVersionEquals(expected); assertTestMethodsNotPresent(); } private void assertVersion45() throws Exception { assertRVersionEquals(45); assertSVersionEquals(45); assertTestMethodsPresent(); } private void assertTestMethodsNotPresent() throws Exception { assertTrue(broadcastForBoolean("MAKE_CALLS_DEFAULT", null)); } private void assertTestMethodsPresent() throws Exception { if (isAtLeastS()) { assertTrue(broadcastForBoolean("MAKE_CALLS_45", null)); } else { // The APIs in the test apex are not currently getting installed correctly // on Android R devices because they rely on the dynamic classpath feature. // TODO(b/234361913): fix this assertTestMethodsNotPresent(); } } private void assertRVersionEquals(int version) throws Exception { int appValue = broadcastForInt("GET_SDK_VERSION", "r"); String syspropValue = getExtensionVersionFromSysprop("r"); assertEquals(version, appValue); assertEquals(String.valueOf(version), syspropValue); assertEquals(version >= 12, canInstallApp(APP_R12_FILENAME, APP_R12_PACKAGE)); assertEquals(version >= 45, canInstallApp(APP_R45_FILENAME, APP_R45_PACKAGE)); } private void assertSVersionEquals(int version) throws Exception { int appValue = broadcastForInt("GET_SDK_VERSION", "s"); String syspropValue = getExtensionVersionFromSysprop("s"); if (isAtLeastS()) { assertEquals(version, appValue); assertEquals(String.valueOf(version), syspropValue); // These APKs require the same R version as they do S version. int minVersion = Math.min(version, broadcastForInt("GET_SDK_VERSION", "r")); assertEquals(minVersion >= 12, canInstallApp(APP_S12_FILENAME, APP_S12_PACKAGE)); assertEquals(minVersion >= 45, canInstallApp(APP_S45_FILENAME, APP_S45_PACKAGE)); } else { assertEquals(0, appValue); assertEquals("", syspropValue); assertFalse(canInstallApp(APP_S12_FILENAME, APP_S12_PACKAGE)); assertFalse(canInstallApp(APP_S45_FILENAME, APP_S45_PACKAGE)); } } private static String getBroadcastCommand(String action, String extra) { String cmd = "am broadcast"; cmd += " -a com.android.sdkext.extensions.apps." + action; if (extra != null) { cmd += " -e extra " + extra; } cmd += " -n com.android.sdkext.extensions.apps/.Receiver"; return cmd; } private boolean isAtLeastS() throws Exception { if (mIsAtLeastS == null) { mIsAtLeastS = broadcastForBoolean("IS_AT_LEAST", "s"); } return mIsAtLeastS; } private boolean isAtLeastT() throws Exception { if (mIsAtLeastT == null) { mIsAtLeastT = broadcastForBoolean("IS_AT_LEAST", "t"); } return mIsAtLeastT; } private boolean uninstallApexes(String... filenames) throws Exception { boolean reboot = false; for (String filename : filenames) { ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(filename)); String res = getDevice().uninstallPackage(apex.name); // res is null for successful uninstalls (non-null likely implesfactory version). reboot |= res == null; } if (reboot) { reboot(); return true; } return false; } private void reboot() throws Exception { getDevice().reboot(); boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis()); assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue(); } }