Add support for always outlining extension APIs
This adds initial support through the system property
com.android.tools.r8.androidApiExtensionPackages
for specifying one or more package prefixes (java syntax, comma separated)
for Android extension APIs.
All classes, methods and fields in the extension packages are treated as
having an API level higher than any known API level thereby "simulating"
that these API might not be available no matter which min API level is
set for the compilation.
RELNOTE: Only mention this if AGP support is added as well.
Bug: b/326252366
Change-Id: I3221440461813b71b76935876033994e6eb05f6c
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
index aaf01ff..bca00e0 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelCompute.java
@@ -21,7 +21,7 @@
public AndroidApiLevelCompute() {
knownApiLevelCache = new KnownApiLevel[AndroidApiLevel.API_DATABASE_LEVEL.getLevel() + 1];
for (AndroidApiLevel value : AndroidApiLevel.values()) {
- if (value != AndroidApiLevel.MAIN) {
+ if (value != AndroidApiLevel.MAIN && value != AndroidApiLevel.EXTENSION) {
knownApiLevelCache[value.getLevel()] = new KnownApiLevel(value);
}
}
@@ -29,7 +29,10 @@
public KnownApiLevel of(AndroidApiLevel apiLevel) {
if (apiLevel == AndroidApiLevel.MAIN) {
- return ComputedApiLevel.master();
+ return ComputedApiLevel.main();
+ }
+ if (apiLevel == AndroidApiLevel.EXTENSION) {
+ return ComputedApiLevel.extension();
}
return knownApiLevelCache[apiLevel.getLevel()];
}
@@ -74,7 +77,7 @@
public ComputedApiLevel computeInitialMinApiLevel(InternalOptions options) {
if (options.getMinApiLevel() == AndroidApiLevel.MAIN) {
- return ComputedApiLevel.master();
+ return ComputedApiLevel.main();
}
return new KnownApiLevel(options.getMinApiLevel());
}
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
index 04f10d8..a08de1f 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
@@ -13,10 +13,15 @@
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ThrowingCharIterator;
import com.android.tools.r8.utils.ThrowingFunction;
+import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.UTFDataFormatException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -30,6 +35,8 @@
private static final byte[] NON_EXISTING_DESCRIPTOR = new byte[0];
+ private final List<DexString> androidApiExtensionPackages;
+
public static byte[] getNonExistingDescriptor() {
return NON_EXISTING_DESCRIPTOR;
}
@@ -158,6 +165,22 @@
predefinedApiReference.getReference(),
Optional.of(predefinedApiReference.getApiLevel()));
});
+ ImmutableList.Builder<DexString> androidApiExtensionPackagesBuilder =
+ new ImmutableList.Builder<>();
+ if (options.apiModelingOptions().androidApiExtensionPackages != null) {
+ StringUtils.split(options.apiModelingOptions().androidApiExtensionPackages, ',')
+ .forEach(
+ pkg -> {
+ androidApiExtensionPackagesBuilder.add(
+ options.itemFactory.createString(
+ "L"
+ + pkg.replace(
+ DescriptorUtils.JAVA_PACKAGE_SEPARATOR,
+ DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR)
+ + "/"));
+ });
+ }
+ this.androidApiExtensionPackages = androidApiExtensionPackagesBuilder.build();
assert predefinedApiTypeLookup.stream()
.allMatch(added -> added.getApiLevel().isEqualTo(lookupApiLevel(added.getReference())));
}
@@ -177,7 +200,39 @@
return lookupApiLevel(field);
}
+ // CHeck that extensionPackage is the exact/full package of the descriptor.
+ private boolean isPackageOfClass(DexString extensionPackage, DexString descriptor) {
+ int packageLengthInBytes = extensionPackage.content.length;
+ // DexString content bytes has a terminating '\0'.
+ assert descriptor.content[packageLengthInBytes - 2] == '/';
+ ThrowingCharIterator<UTFDataFormatException> charIterator =
+ descriptor.iterator(packageLengthInBytes - 1);
+ while (charIterator.hasNext()) {
+ try {
+ if (charIterator.nextChar() == '/') {
+ // Found another package separator, som not exact package.
+ return false;
+ }
+ } catch (UTFDataFormatException e) {
+ assert false
+ : "Iterating " + descriptor + " from index " + packageLengthInBytes + " caused " + e;
+ return false;
+ }
+ }
+ return true;
+ }
+
private AndroidApiLevel lookupApiLevel(DexReference reference) {
+ // TODO(b/326252366): Assigning all extension items the same "fake" API level results in API
+ // outlines becoming mergable across extensions, which should be prevented.
+ for (int i = 0; i < androidApiExtensionPackages.size(); i++) {
+ DexString descriptor = reference.getContextType().getDescriptor();
+ DexString extensionPackage = androidApiExtensionPackages.get(i);
+ if (descriptor.startsWith(extensionPackage)
+ && isPackageOfClass(extensionPackage, descriptor)) {
+ return AndroidApiLevel.EXTENSION;
+ }
+ }
Optional<AndroidApiLevel> result =
lookupCache.computeIfAbsent(
reference,
diff --git a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
index 5e5c280..363c6e4 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
@@ -23,8 +23,12 @@
return UnknownApiLevel.INSTANCE;
}
- static KnownApiLevel master() {
- return KnownApiLevel.MASTER_INSTANCE;
+ static KnownApiLevel main() {
+ return KnownApiLevel.MAIN_INSTANCE;
+ }
+
+ static KnownApiLevel extension() {
+ return KnownApiLevel.EXTENSION_INSTANCE;
}
default boolean isNotSetApiLevel() {
@@ -161,7 +165,9 @@
class KnownApiLevel implements ComputedApiLevel {
- private static final KnownApiLevel MASTER_INSTANCE = new KnownApiLevel(AndroidApiLevel.MAIN);
+ private static final KnownApiLevel MAIN_INSTANCE = new KnownApiLevel(AndroidApiLevel.MAIN);
+ private static final KnownApiLevel EXTENSION_INSTANCE =
+ new KnownApiLevel(AndroidApiLevel.EXTENSION);
private final AndroidApiLevel apiLevel;
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 00a15e8..cae4a0e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -198,9 +198,13 @@
}
public ThrowingCharIterator<UTFDataFormatException> iterator() {
+ return iterator(0);
+ }
+
+ public ThrowingCharIterator<UTFDataFormatException> iterator(int startIndex) {
return new ThrowingCharIterator<UTFDataFormatException>() {
- private int i = 0;
+ private int i = startIndex;
@Override
public char nextChar() throws UTFDataFormatException {
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
index 05c0b45..fbb8aa6 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -44,7 +44,8 @@
Sv2(32),
T(33),
U(34),
- MAIN(35); // API level for main is tentative.
+ MAIN(35), // API level for main is tentative.
+ EXTENSION(Integer.MAX_VALUE); // Used for API modeling of Android extension APIs.
// When updating LATEST and a new version goes public, add a new api-versions.xml to third_party
// and update the version and generated jar in AndroidApiDatabaseBuilderGeneratorTest. Together
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index f61b479..d89369a 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2002,6 +2002,16 @@
public boolean enableLibraryApiModeling =
System.getProperty("com.android.tools.r8.disableApiModeling") == null;
+ // Flag to specify packages for Android extension APIs (also known as OEM-implemented
+ // shared libraries or sidecars). The packages are specified as java package names
+ // separated by commas. All APIs within these packages are handled as having an API level
+ // higher than any existing API level as these APIs might not exist on any device independent
+ // of API level (the nature of an extension API).
+ // TODO(b/326252366): This mechanism should be extended to also specify the extension for
+ // each package to prevent merging of API outline methods fron different extensions.
+ public String androidApiExtensionPackages =
+ System.getProperty("com.android.tools.r8.androidApiExtensionPackages");
+
// The flag enableApiCallerIdentification controls if we can inline or merge targets with
// different api levels. It is also the flag that specifies if we assign api levels to
// references.
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiMultipleExtensionPackagesTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiMultipleExtensionPackagesTest.java
new file mode 100644
index 0000000..0c3555a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiMultipleExtensionPackagesTest.java
@@ -0,0 +1,129 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.apimodel.another_extension.AnotherExtensionApiLibraryClass;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryClass;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelOutlineMethodExtensionApiMultipleExtensionPackagesTest extends TestBase {
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "ExtensionApiLibraryClass::extensionApi",
+ "AnotherExtensionApiLibraryClass::extensionApi");
+
+ @Parameter public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addLibraryClasses(ExtensionApiLibraryClass.class, AnotherExtensionApiLibraryClass.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .addOptionsModification(
+ options -> {
+ String typeName = ExtensionApiLibraryClass.class.getTypeName();
+ String anotherTypeName = AnotherExtensionApiLibraryClass.class.getTypeName();
+ options.apiModelingOptions().androidApiExtensionPackages =
+ typeName.substring(0, typeName.lastIndexOf('.'))
+ + ','
+ + anotherTypeName.substring(0, anotherTypeName.lastIndexOf('.'))
+ + ','
+ + typeName.substring(0, typeName.lastIndexOf('.'));
+ })
+ // TODO(b/213552119): Remove when enabled by default.
+ .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+ .apply(ApiModelingTestHelper::disableStubbingOfClasses);
+ }
+
+ private void populateBootClasspath(TestCompileResult<?, ?> compilerResult) throws Exception {
+ compilerResult.addBootClasspathClasses(
+ ExtensionApiLibraryClass.class, AnotherExtensionApiLibraryClass.class);
+ }
+
+ @Test
+ public void testD8Debug() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.DEBUG)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testD8Release() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.RELEASE)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .apply(this::setupTestBuilder)
+ .addKeepMainRule(Main.class)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ private void inspect(CodeInspector inspector) throws Exception {
+ assertThat(inspector.clazz(ExtensionApiLibraryClass.class), isAbsent());
+ verifyThat(
+ inspector, parameters, ExtensionApiLibraryClass.class.getDeclaredMethod("extensionApi"))
+ .isOutlinedFromIf(
+ Main.class.getDeclaredMethod("main", String[].class), parameters.isDexRuntime());
+ verifyThat(
+ inspector,
+ parameters,
+ AnotherExtensionApiLibraryClass.class.getDeclaredMethod("extensionApi"))
+ .isOutlinedFromIf(
+ Main.class.getDeclaredMethod("main", String[].class), parameters.isDexRuntime());
+ }
+
+ public static class Main {
+ public static void main(String[] args) {
+ ExtensionApiLibraryClass.extensionApi();
+ AnotherExtensionApiLibraryClass.extensionApi();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiSubpackageTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiSubpackageTest.java
new file mode 100644
index 0000000..896793f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiSubpackageTest.java
@@ -0,0 +1,148 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryClass;
+import com.android.tools.r8.apimodel.extension.subpackage.SubpackageExtensionApiLibraryClass;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelOutlineMethodExtensionApiSubpackageTest extends TestBase {
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "ExtensionApiLibraryClass::extensionApi",
+ "SubpackageExtensionApiLibraryClass::extensionApi");
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public boolean packageIsExtension;
+
+ @Parameter(2)
+ public boolean subpackageIsExtension;
+
+ @Parameters(name = "{0}, packageIsExtension = {1}, subpackageIsExtension = {2}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimesAndApiLevels().build(),
+ BooleanUtils.values(),
+ BooleanUtils.values());
+ }
+
+ private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addLibraryClasses(ExtensionApiLibraryClass.class, SubpackageExtensionApiLibraryClass.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .addOptionsModification(
+ options -> {
+ StringBuilder androidApiExtensionPackages = new StringBuilder();
+ if (packageIsExtension) {
+ String typeName = ExtensionApiLibraryClass.class.getTypeName();
+ androidApiExtensionPackages.append(typeName, 0, typeName.lastIndexOf('.'));
+ }
+ if (subpackageIsExtension) {
+ String typeName = SubpackageExtensionApiLibraryClass.class.getTypeName();
+ if (packageIsExtension) {
+ androidApiExtensionPackages.append(",");
+ }
+ androidApiExtensionPackages.append(typeName, 0, typeName.lastIndexOf('.'));
+ }
+ options.apiModelingOptions().androidApiExtensionPackages =
+ androidApiExtensionPackages.toString();
+ })
+ // TODO(b/213552119): Remove when enabled by default.
+ .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+ .apply(ApiModelingTestHelper::disableStubbingOfClasses);
+ }
+
+ private void populateBootClasspath(TestCompileResult<?, ?> compilerResult) throws Exception {
+ compilerResult.addBootClasspathClasses(
+ ExtensionApiLibraryClass.class, SubpackageExtensionApiLibraryClass.class);
+ }
+
+ @Test
+ public void testD8Debug() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.DEBUG)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testD8Release() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.RELEASE)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .apply(this::setupTestBuilder)
+ .addKeepMainRule(Main.class)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ private void inspect(CodeInspector inspector) throws Exception {
+ assertThat(inspector.clazz(ExtensionApiLibraryClass.class), isAbsent());
+ verifyThat(
+ inspector, parameters, ExtensionApiLibraryClass.class.getDeclaredMethod("extensionApi"))
+ .isOutlinedFromIf(
+ Main.class.getDeclaredMethod("main", String[].class),
+ packageIsExtension && parameters.isDexRuntime());
+ verifyThat(
+ inspector,
+ parameters,
+ SubpackageExtensionApiLibraryClass.class.getDeclaredMethod("extensionApi"))
+ .isOutlinedFromIf(
+ Main.class.getDeclaredMethod("main", String[].class),
+ subpackageIsExtension && parameters.isDexRuntime());
+ }
+
+ public static class Main {
+ public static void main(String[] args) {
+ ExtensionApiLibraryClass.extensionApi();
+ SubpackageExtensionApiLibraryClass.extensionApi();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiTest.java
new file mode 100644
index 0000000..2dd7acd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodExtensionApiTest.java
@@ -0,0 +1,148 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryClass;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryInterface;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryInterfaceImpl;
+import com.android.tools.r8.apimodel.extension.ExtensionApiLibraryInterfaceProvider;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelOutlineMethodExtensionApiTest extends TestBase {
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "ExtensionApiLibraryClass::extensionApi",
+ "ExtensionApiLibraryInterfaceImpl::extensionApi");
+
+ @Parameter public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addLibraryClasses(
+ ExtensionApiLibraryClass.class,
+ ExtensionApiLibraryInterface.class,
+ ExtensionApiLibraryInterfaceProvider.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .addOptionsModification(
+ options -> {
+ String typeName = ExtensionApiLibraryClass.class.getTypeName();
+ options.apiModelingOptions().androidApiExtensionPackages =
+ typeName.substring(0, typeName.lastIndexOf('.'));
+ })
+ // TODO(b/213552119): Remove when enabled by default.
+ .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+ .apply(ApiModelingTestHelper::disableStubbingOfClasses);
+ }
+
+ private void populateBootClasspath(TestCompileResult<?, ?> compilerResult) throws Exception {
+ compilerResult.addBootClasspathClasses(
+ ExtensionApiLibraryClass.class,
+ ExtensionApiLibraryInterface.class,
+ ExtensionApiLibraryInterfaceImpl.class,
+ ExtensionApiLibraryInterfaceProvider.class);
+ }
+
+ @Test
+ public void testD8Debug() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.DEBUG)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testD8Release() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .setMode(CompilationMode.RELEASE)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .apply(this::setupTestBuilder)
+ .addKeepMainRule(Main.class)
+ .enableInliningAnnotations()
+ .compile()
+ .apply(this::populateBootClasspath)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ private void inspect(CodeInspector inspector) throws Exception {
+ assertThat(inspector.clazz(ExtensionApiLibraryClass.class), isAbsent());
+ verifyThat(
+ inspector, parameters, ExtensionApiLibraryClass.class.getDeclaredMethod("extensionApi"))
+ .isOutlinedFromIf(
+ Main.class.getDeclaredMethod("main", String[].class), parameters.isDexRuntime());
+ verifyThat(
+ inspector,
+ parameters,
+ ExtensionApiLibraryInterface.class.getDeclaredMethod("extensionInterfaceApi"))
+ .isOutlinedFromIf(
+ ExtensionApiUtil.class.getDeclaredMethod("invokeExtensionInterfaceApi"),
+ parameters.isDexRuntime());
+ }
+
+ public static class ExtensionApiUtil {
+ private static final ExtensionApiLibraryInterface api;
+
+ static {
+ api = ExtensionApiLibraryInterfaceProvider.getProvider();
+ }
+
+ @NeverInline
+ public static void invokeExtensionInterfaceApi() {
+ api.extensionInterfaceApi();
+ }
+ }
+
+ public static class Main {
+ public static void main(String[] args) {
+ ExtensionApiLibraryClass.extensionApi();
+ ExtensionApiUtil.invokeExtensionInterfaceApi();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index c546154..f369ed9 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -502,6 +502,14 @@
}
}
+ void isOutlinedFromIf(Executable method, boolean condition) {
+ if (condition) {
+ isOutlinedFrom(method);
+ } else {
+ isNotOutlinedFrom(method);
+ }
+ }
+
void isOutlinedFrom(Executable method) {
// Check that the call is in a synthetic class.
List<FoundMethodSubject> outlinedMethod =
diff --git a/src/test/java/com/android/tools/r8/apimodel/another_extension/AnotherExtensionApiLibraryClass.java b/src/test/java/com/android/tools/r8/apimodel/another_extension/AnotherExtensionApiLibraryClass.java
new file mode 100644
index 0000000..4dcf1ef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/another_extension/AnotherExtensionApiLibraryClass.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.another_extension;
+
+public class AnotherExtensionApiLibraryClass {
+
+ public static void extensionApi() {
+ System.out.println("AnotherExtensionApiLibraryClass::extensionApi");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryClass.java b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryClass.java
new file mode 100644
index 0000000..3fb0f9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryClass.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.extension;
+
+public class ExtensionApiLibraryClass {
+
+ public static void extensionApi() {
+ System.out.println("ExtensionApiLibraryClass::extensionApi");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterface.java b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterface.java
new file mode 100644
index 0000000..bb84d51
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterface.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.extension;
+
+public interface ExtensionApiLibraryInterface {
+
+ default void extensionInterfaceApi() {
+ System.out.println("ExtensionApiLibraryInterface::extensionApi");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceImpl.java b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceImpl.java
new file mode 100644
index 0000000..ef4c860
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceImpl.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.extension;
+
+public class ExtensionApiLibraryInterfaceImpl implements ExtensionApiLibraryInterface {
+
+ @Override
+ public void extensionInterfaceApi() {
+ System.out.println("ExtensionApiLibraryInterfaceImpl::extensionApi");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceProvider.java b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceProvider.java
new file mode 100644
index 0000000..13d9ae1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/extension/ExtensionApiLibraryInterfaceProvider.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.extension;
+
+public class ExtensionApiLibraryInterfaceProvider {
+
+ public static ExtensionApiLibraryInterface getProvider() {
+ return new ExtensionApiLibraryInterfaceImpl();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/extension/subpackage/SubpackageExtensionApiLibraryClass.java b/src/test/java/com/android/tools/r8/apimodel/extension/subpackage/SubpackageExtensionApiLibraryClass.java
new file mode 100644
index 0000000..52f1c18
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/extension/subpackage/SubpackageExtensionApiLibraryClass.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel.extension.subpackage;
+
+public class SubpackageExtensionApiLibraryClass {
+
+ public static void extensionApi() {
+ System.out.println("SubpackageExtensionApiLibraryClass::extensionApi");
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index 8fd3b6b..93b6fda 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -1263,6 +1263,7 @@
case J_MR1:
case J_MR2:
case K_WATCH:
+ case EXTENSION:
return false;
default:
return true;