blob: a9337e3c29e86df9b1db4cf7bba6600f075b46b0 [file] [log] [blame]
/*
* Copyright (C) 2018 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.build.gradle.integration.databinding
import com.android.build.gradle.integration.common.fixture.GradleTestProject
import com.android.build.gradle.integration.common.fixture.app.MinimalSubProject
import com.android.build.gradle.integration.common.fixture.app.MultiModuleTestProject
import com.android.build.gradle.integration.common.runner.FilterableParameterized
import com.android.build.gradle.internal.scope.CodeShrinker
import com.android.build.gradle.options.BooleanOption
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* Tests using Proguard/R8 to shrink and obfuscate code in a project with features.
*
* Project roughly structured as follows (see implementation below for exact structure) :
*
* <pre>
* ---> library2 ------>
* otherFeature1 ---> library3 library1
* ---> baseModule ---->
* otherFeature2
*
* More explicitly,
* instantApp depends on otherFeature1, otherFeature2, baseModule (not pictured)
* app depends on otherFeature1, otherFeature2, baseModule (not pictured)
* otherFeature1 depends on library2, library3, baseModule
* otherFeature2 depends on baseModule
* baseModule depends on library1
* library2 depends on library1
* </pre>
*
* This test is taken from [MinifyFeaturesTest] and modified to enable data binding.
*/
@RunWith(FilterableParameterized::class)
class DataBindingMinifyFeaturesTest(
val codeShrinker: CodeShrinker,
val multiApkMode: MultiApkMode) {
enum class MultiApkMode {
DYNAMIC_APP, INSTANT_APP
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "codeShrinker {0}, {1}")
fun getConfigurations(): Collection<Array<Enum<*>>> =
listOf(
arrayOf(CodeShrinker.PROGUARD, MultiApkMode.DYNAMIC_APP),
arrayOf(CodeShrinker.PROGUARD, MultiApkMode.INSTANT_APP),
arrayOf(CodeShrinker.R8, MultiApkMode.DYNAMIC_APP),
arrayOf(CodeShrinker.R8, MultiApkMode.INSTANT_APP)
)
}
private val otherFeature2GradlePath = ":otherFeature2"
private val lib1 =
MinimalSubProject.lib("com.example.lib1")
.appendToBuild("""
android {
dataBinding.enabled = true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
consumerProguardFiles "proguard-rules.pro"
}
}
}
""")
.withFile("src/main/resources/lib1_java_res.txt", "lib1")
.withFile(
"src/main/java/com/example/lib1/Lib1Class.java",
"""package com.example.lib1;
import java.io.InputStream;
public class Lib1Class {
public String getJavaRes() {
InputStream inputStream =
Lib1Class.class
.getClassLoader()
.getResourceAsStream("lib1_java_res.txt");
if (inputStream == null) {
return "can't find lib1_java_res";
}
byte[] line = new byte[1024];
try {
inputStream.read(line);
return new String(line, "UTF-8").trim();
} catch (Exception ignore) {
}
return "something went wrong";
}
}""")
.withFile(
"src/main/java/com/example/lib1/EmptyClassToKeep.java",
"""package com.example.lib1;
public class EmptyClassToKeep {
}""")
.withFile(
"src/main/java/com/example/lib1/EmptyClassToRemove.java",
"""package com.example.lib1;
public class EmptyClassToRemove {
}""")
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.lib1.EmptyClassToKeep""")
private val lib2 =
MinimalSubProject.lib("com.example.lib2")
.appendToBuild("""
android {
dataBinding.enabled = true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
consumerProguardFiles "proguard-rules.pro"
}
}
}
""")
// include foo_view.xml and FooView.java below to generate aapt proguard rules to be
// merged in the base.
.withFile(
"src/main/res/layout/foo_view.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<view
xmlns:android="http://schemas.android.com/apk/res/android"
class="com.example.lib2.FooView"
android:id="@+id/foo_view" />"""
)
.withFile(
"src/main/java/com/example/lib2/FooView.java",
"""package com.example.lib2;
import android.content.Context;
import android.view.View;
public class FooView extends View {
public FooView(Context context) {
super(context);
}
}""")
.withFile("src/main/resources/lib2_java_res.txt", "lib2")
.withFile(
"src/main/java/com/example/lib2/Lib2Class.java",
"""package com.example.lib2;
import java.io.InputStream;
public class Lib2Class {
public String getJavaRes() {
InputStream inputStream =
Lib2Class.class
.getClassLoader()
.getResourceAsStream("lib2_java_res.txt");
if (inputStream == null) {
return "can't find lib2_java_res";
}
byte[] line = new byte[1024];
try {
inputStream.read(line);
return new String(line, "UTF-8").trim();
} catch (Exception ignore) {
}
return "something went wrong";
}
}""")
.withFile(
"src/main/java/com/example/lib2/EmptyClassToKeep.java",
"""package com.example.lib2;
public class EmptyClassToKeep {
}""")
.withFile(
"src/main/java/com/example/lib2/EmptyClassToRemove.java",
"""package com.example.lib2;
public class EmptyClassToRemove {
}""")
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.lib2.EmptyClassToKeep""")
private val lib3 =
MinimalSubProject.lib("com.example.lib3")
.appendToBuild("android { buildTypes { minified { initWith(buildTypes.debug) }}}")
private val baseModule =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.app("com.example.baseModule")
.appendToBuild(
"""
android {
dataBinding.enabled = true
dynamicFeatures = [':foo:otherFeature1', '$otherFeature2GradlePath']
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt'),
"proguard-rules.pro"
}
}
}
"""
)
MultiApkMode.INSTANT_APP ->
MinimalSubProject.feature("com.example.baseModule")
.appendToBuild(
"""
android {
dataBinding.enabled = true
baseFeature true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt')
consumerProguardFiles "proguard-rules.pro"
}
}
}
"""
)
}.let {
it
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.baseModule">
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/base_main.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Base"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile(
"src/main/res/values/string.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="otherFeature1">otherFeature1</string>
<string name="otherFeature2">otherFeature2</string>
</resources>"""
)
.withFile("src/main/resources/base_java_res.txt", "base")
.withFile(
"src/main/java/com/example/baseModule/Main.java",
"""package com.example.baseModule;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.lib1.Lib1Class;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
private final Lib1Class lib1Class = new Lib1Class();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.base_main);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(
""
+ getLib1Class().getJavaRes()
+ " "
+ getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public Lib1Class getLib1Class() {
return lib1Class;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
.withFile(
"src/main/java/com/example/baseModule/StringProvider.java",
"""package com.example.baseModule;
public class StringProvider {
public String getString(int foo) {
return Integer.toString(foo);
}
}"""
)
.withFile(
"src/main/java/com/example/baseModule/EmptyClassToKeep.java",
"""package com.example.baseModule;
public class EmptyClassToKeep {
}"""
)
.withFile(
"src/main/java/com/example/baseModule/EmptyClassToRemove.java",
"""package com.example.baseModule;
public class EmptyClassToRemove {
}"""
)
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.baseModule.EmptyClassToKeep"""
)
}
private val otherFeature1 =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.dynamicFeature("com.example.otherFeature1")
MultiApkMode.INSTANT_APP -> MinimalSubProject.feature("com.example.otherFeature1")
}.let { minimalSubProject ->
val proguardFilesDsl =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> "proguardFiles"
MultiApkMode.INSTANT_APP -> "consumerProguardFiles"
}
minimalSubProject
.appendToBuild(
"""
android {
dataBinding.enabled = true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
$proguardFilesDsl "proguard-rules.pro"
}
}
}
"""
)
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.otherFeature1">
<dist:module dist:onDemand="true"
dist:title="@string/otherFeature1">
<dist:fusing dist:include="true"/>
</dist:module>
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/other_main_1.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Other Feature 1"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile("src/main/resources/other_java_res_1.txt", "other")
.withFile(
"src/main/java/com/example/otherFeature1/Main.java",
"""package com.example.otherFeature1;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.baseModule.StringProvider;
import com.example.lib2.Lib2Class;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
private final Lib2Class lib2Class = new Lib2Class();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.other_main_1);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(
""
+ getLib2Class().getJavaRes()
+ " "
+ getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public Lib2Class getLib2Class() {
return lib2Class;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
.withFile(
"src/main/java/com/example/otherFeature1/EmptyClassToKeep.java",
"""package com.example.otherFeature1;
public class EmptyClassToKeep {
}"""
)
.withFile(
"src/main/java/com/example/otherFeature1/EmptyClassToRemove.java",
"""package com.example.otherFeature1;
public class EmptyClassToRemove {
}"""
)
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.otherFeature1.EmptyClassToKeep"""
)
}
private val otherFeature2 =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.dynamicFeature("com.example.otherFeature2")
MultiApkMode.INSTANT_APP -> MinimalSubProject.feature("com.example.otherFeature2")
}.let {
it
.appendToBuild("""
android {
dataBinding.enabled = true
buildTypes {
minified.initWith(buildTypes.debug)
}
}
""")
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.otherFeature2">
<dist:module dist:onDemand="true"
dist:title="@string/otherFeature2">
<dist:fusing dist:include="true"/>
</dist:module>
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/other_main_2.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Other Feature 2"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile("src/main/resources/other_java_res_2.txt", "other")
.withFile(
"src/main/java/com/example/otherFeature2/Main.java",
"""package com.example.otherFeature2;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.baseModule.StringProvider;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.other_main_2);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
}
private val app =
MinimalSubProject.app("com.example.app")
.appendToBuild("""
android {
dataBinding.enabled = true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
}
""")
private val instantApp = MinimalSubProject.instantApp()
.appendToBuild("""
android {
dataBinding.enabled = true
buildTypes {
minified
}
}
""".trimIndent())
private val testApp =
MultiModuleTestProject.builder()
.subproject(":lib1", lib1)
.subproject(":lib2", lib2)
.subproject(":lib3", lib3)
.subproject(":baseModule", baseModule)
.subproject(":foo:otherFeature1", otherFeature1)
.subproject(otherFeature2GradlePath, otherFeature2)
.dependency(otherFeature1, lib2)
// otherFeature1 depends on lib3 to test having multiple library module dependencies.
.dependency(otherFeature1, lib3)
.dependency(otherFeature1, baseModule)
.dependency(otherFeature2, baseModule)
.dependency("api", lib2, lib1)
.dependency(baseModule, lib1)
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it
MultiApkMode.INSTANT_APP ->
it
.subproject(":app", app)
.subproject(":instantApp", instantApp)
.dependency(app, baseModule)
.dependency(app, otherFeature1)
.dependency(app, otherFeature2)
.dependency(instantApp, baseModule)
.dependency(instantApp, otherFeature1)
.dependency(instantApp, otherFeature2)
.dependency("application", baseModule, app)
.dependency("feature", baseModule, otherFeature1)
.dependency("feature", baseModule, otherFeature2)
}
}
.build()
@get:Rule
val project = GradleTestProject.builder().fromTestApp(testApp).create()
@Test
fun assembleMinified() {
val executor = project.executor()
.with(BooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
.with(BooleanOption.ENABLE_EXPERIMENTAL_FEATURE_DATABINDING, true)
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> executor.run("assembleMinified")
MultiApkMode.INSTANT_APP -> executor.run("app:assembleMinified", "instantApp:assembleMinified")
}
}
}