summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYigit Boyar <yboyar@google.com>2019-10-03 16:12:46 -0700
committerTreeHugger Robot <treehugger-gerrit@google.com>2019-10-08 21:10:34 +0000
commit103e4e323372565802ecb09ee689b77f43acc92e (patch)
treecb4eacc7ccd6cc6108ffa63aa16eae72561cad2a
parentbff339917a58bccd30775d866fe12f67af166682 (diff)
downloaddata-binding-103e4e323372565802ecb09ee689b77f43acc92e.tar.gz
Detect recursive structures in data binding
This CL fixes two bugs in data binding both related to recursive data structures. If you provide data binding a class that looks like Foo<T : Foo> or Foo : LiveData<T : Foo>, it would go into a stackoverflow or OOM trying to parse it since it would land back into the same class as it tries to resolve values. This CL fixes it by adding a tracker into such code and bails out with whatever information it can. For cases where the class in question is observable (e.g. class Foo : LiveData<Foo>), we crash with a message since data binding will try to create foo.getValue().getValue()... For cases where it is a normal class (which we won't try to unwrap), we work as desired. There is possibly more of these but i can only reproduce these 3 cases so didn't want to overzealously add more coverage without having a repro case. Bug: 141633235 Bug: 140999936 Test: RecursiveLayoutTest (TestApp), RecursiveObservableTest (compileation test) Change-Id: I9977a1c8ae7c726b924550783d48049e89ca227c
-rw-r--r--compilationTests/src/test/java/androidx/databinding/compilationTest/RecursiveObservableTest.kt42
-rw-r--r--compilationTests/src/test/resources/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java23
-rw-r--r--compilationTests/src/test/resources/app_build.gradle3
-rw-r--r--compilationTests/src/test/resources/layout/recursive_layout.xml31
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/Expr.java76
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/RecursiveTraversal.kt91
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationTypeUtil.java107
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java10
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java2
-rw-r--r--extensions-support/gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml19
-rw-r--r--integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/RecursiveLayoutTest.java51
-rw-r--r--integration-tests/TestApp/app/src/main/AndroidManifest.xml20
-rw-r--r--integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/RecursiveAdapter.java29
-rw-r--r--integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/RecursiveClass.java18
-rw-r--r--integration-tests/TestApp/app/src/main/res/layout/recursive_layout.xml36
-rw-r--r--integration-tests/TestApp/app/src/main/res/values/styles.xml5
17 files changed, 466 insertions, 99 deletions
diff --git a/compilationTests/src/test/java/androidx/databinding/compilationTest/RecursiveObservableTest.kt b/compilationTests/src/test/java/androidx/databinding/compilationTest/RecursiveObservableTest.kt
new file mode 100644
index 00000000..0b768e7e
--- /dev/null
+++ b/compilationTests/src/test/java/androidx/databinding/compilationTest/RecursiveObservableTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 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 androidx.databinding.compilationTest
+
+import android.databinding.tool.processing.ErrorMessages
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RecursiveObservableTest : BaseCompilationTest(true) {
+ @Test
+ fun recursiveObservableUsed() {
+ prepareProject()
+ copyResourceTo("/layout/recursive_layout.xml",
+ "/app/src/main/res/layout/recursive.xml")
+ copyResourceTo(
+ "/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java",
+ "/app/src/main/java/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java")
+ val result = runGradle("assembleDebug")
+ assertThat(result.error, result.bindingExceptions.firstOrNull()?.createHumanReadableMessage(),
+ containsString(
+ String.format(ErrorMessages.RECURSIVE_OBSERVABLE, "recursiveLiveData.text")
+ ))
+ }
+} \ No newline at end of file
diff --git a/compilationTests/src/test/resources/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java b/compilationTests/src/test/resources/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java
new file mode 100644
index 00000000..aa961585
--- /dev/null
+++ b/compilationTests/src/test/resources/androidx/databinding/compilationTest/badJava/RecursiveLiveData.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 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 androidx.databinding.compilationTest.badJava;
+
+import androidx.lifecycle.LiveData;
+
+public class RecursiveLiveData extends LiveData<RecursiveLiveData> {
+ public String text;
+}
diff --git a/compilationTests/src/test/resources/app_build.gradle b/compilationTests/src/test/resources/app_build.gradle
index eb32117d..e737bee9 100644
--- a/compilationTests/src/test/resources/app_build.gradle
+++ b/compilationTests/src/test/resources/app_build.gradle
@@ -24,6 +24,7 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
- compile "com.android.support:support-v4:26.1.0"
+ implementation "com.android.support:support-v4:26.1.0"
+ implementation "androidx.lifecycle:lifecycle-livedata:2.0.0"
!@{DEPENDENCIES}
}
diff --git a/compilationTests/src/test/resources/layout/recursive_layout.xml b/compilationTests/src/test/resources/layout/recursive_layout.xml
new file mode 100644
index 00000000..86123146
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/recursive_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <data>
+ <variable name="recursiveLiveData" type="androidx.databinding.compilationTest.badJava.RecursiveLiveData" />
+ </data>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/recursiveLiveDataText"
+ android:text="@{recursiveLiveData.text}"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </LinearLayout>
+</layout> \ No newline at end of file
diff --git a/compiler/src/main/java/android/databinding/tool/expr/Expr.java b/compiler/src/main/java/android/databinding/tool/expr/Expr.java
index fe4953ac..f972af65 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/Expr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/Expr.java
@@ -22,6 +22,8 @@ import android.databinding.tool.processing.scopes.LocationScopeProvider;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelMethod;
+import android.databinding.tool.reflection.RecursionTracker;
+import android.databinding.tool.reflection.RecursiveResolutionStack;
import android.databinding.tool.solver.ExecutionPath;
import android.databinding.tool.store.Location;
import android.databinding.tool.util.L;
@@ -29,6 +31,7 @@ import android.databinding.tool.util.Preconditions;
import android.databinding.tool.writer.KCode;
import android.databinding.tool.writer.LayoutBinderWriterKt;
+import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -116,6 +119,9 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
private boolean mIsUsedInCallback = false;
private boolean mUnwrapObservableFields = true;
+ // used to prevent infinite loops when resolving recursive data structures
+ private static RecursiveResolutionStack sResolveTypeStack = new RecursiveResolutionStack();
+
Expr(Iterable<Expr> children) {
for (Expr expr : children) {
mChildren.add(expr);
@@ -357,21 +363,30 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
}
public final ModelClass getResolvedType() {
- if (mResolvedType == null) {
- if (mUnwrapObservableFields) {
- unwrapObservableFieldChildren();
- mUnwrapObservableFields = false;
- }
- // TODO not get instance
- try {
- Scope.enter(this);
- mResolvedType = resolveType(ModelAnalyzer.getInstance());
- if (mResolvedType == null) {
- L.e(ErrorMessages.CANNOT_RESOLVE_TYPE, this);
- }
- } finally {
- Scope.exit();
+ if (mResolvedType != null) {
+ return mResolvedType;
+ }
+ try {
+ Scope.enter(this);
+ mResolvedType = sResolveTypeStack.visit(
+ this,
+ currentType -> {
+ if (mUnwrapObservableFields) {
+ unwrapObservableFieldChildren();
+ mUnwrapObservableFields = false;
+ }
+ return resolveType(ModelAnalyzer.getInstance());
+ },
+ recursedType -> {
+ // solve without unwrapping observables
+ return resolveType(ModelAnalyzer.getInstance());
+ }
+ );
+ if (mResolvedType == null) {
+ L.e(ErrorMessages.CANNOT_RESOLVE_TYPE, this);
}
+ } finally {
+ Scope.exit();
}
return mResolvedType;
}
@@ -863,9 +878,19 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
}
public Expr unwrapObservableField() {
+ final RecursionTracker<ModelClass> recursionTracker = new RecursionTracker<>(recursed -> {
+ if (recursed.isObservable()) {
+ L.e(ErrorMessages.RECURSIVE_OBSERVABLE, recursed);
+ } else {
+ L.w("Observable field resolved into another observable, skipping resolution. %s", recursed);
+ }
+ return Unit.INSTANCE;
+ });
+
Expr expr = this;
String simpleGetterName;
- while ((simpleGetterName = expr.getResolvedType().getObservableGetterName()) != null) {
+ while ((simpleGetterName = expr.getResolvedType().getObservableGetterName()) != null
+ && recursionTracker.pushIfNew(expr.getResolvedType())) {
Expr unwrapped = mModel.methodCall(expr, simpleGetterName, Collections.EMPTY_LIST);
mModel.bindingExpr(unwrapped);
unwrapped.setUnwrapObservableFields(false);
@@ -891,26 +916,25 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
* @param type The expected type or null if the child should be fully unwrapped.
*/
protected void unwrapChildTo(int childIndex, @Nullable ModelClass type) {
+ final RecursionTracker<ModelClass> recursionTracker = new RecursionTracker<>(recursed -> {
+ if (recursed.isObservable()) {
+ L.e(ErrorMessages.RECURSIVE_OBSERVABLE, this);
+ } else {
+ L.d("Recursed while resolving %s, will stop resolution.", recursed);
+ }
+ return Unit.INSTANCE;
+ });
final Expr child = mChildren.get(childIndex);
Expr unwrapped = null;
Expr expr = child;
String simpleGetterName;
while ((simpleGetterName = expr.getResolvedType().getObservableGetterName()) != null
+ && recursionTracker.pushIfNew(expr.getResolvedType())
&& shouldUnwrap(type, expr.getResolvedType())) {
unwrapped = mModel.methodCall(expr, simpleGetterName, Collections.EMPTY_LIST);
- if (unwrapped == this) {
- if (type != null) {
- if (type.isObservableField()) {
- L.w(ErrorMessages.OBSERVABLE_FIELD_GET, this);
- }
- else if (type.isLiveData()) {
- L.w(ErrorMessages.LIVEDATA_FIELD_GETVALUE, this);
- }
- }
- return; // This was already unwrapped!
- }
unwrapped.setUnwrapObservableFields(false);
expr = unwrapped;
+
}
if (unwrapped != null) {
child.getParents().remove(this);
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/RecursiveTraversal.kt b/compiler/src/main/java/android/databinding/tool/reflection/RecursiveTraversal.kt
new file mode 100644
index 00000000..7ce76bcb
--- /dev/null
+++ b/compiler/src/main/java/android/databinding/tool/reflection/RecursiveTraversal.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 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 android.databinding.tool.reflection
+
+import android.databinding.tool.util.L
+import java.util.*
+
+/**
+ * This class helps with recursive resolution code (like resolving a type, printing java etc) and avoids going into
+ * an infinite loop if the type is recursive or contains itself in an inner loop.
+ *
+ * It keeps track of local objects and detect if a recursion happens, delegating to the callback to return whatever
+ * is desired.
+ *
+ * Directly use this one if you know the scope or if you don't (e.g. function being called multiple times), use
+ * [RecursiveResolutionStack].
+ */
+class RecursionTracker<T>(
+ /**
+ * Called when error is discovered so that client can provide a better warning or just debug
+ */
+ private val errorReporter: (T) -> Unit
+) {
+ // current items in stack
+ private val items = ArrayDeque<T>()
+
+ /**
+ * Adds the item to the list of tracked items if it is not there already.
+ * Returns true if added, false otherwise.
+ */
+ fun pushIfNew(item: T): Boolean {
+ if (items.contains(item)) {
+ errorReporter(item)
+ return false
+ }
+ items.push(item)
+ return true
+ }
+
+ /**
+ * Removes the last element from stack and checks it is the expected element to detect buggy code which may not
+ * control stack properly.
+ */
+ @Throws(IllegalStateException::class)
+ fun popAndCheck(item: T) {
+ val removed = items.pop()
+ check(item == removed) {
+ "inconsistent reference stack. received $removed expected $item"
+ }
+ }
+}
+
+class RecursiveResolutionStack {
+ /**
+ * List of items. Using a thread local here to be able to maintain it across multiple calls
+ */
+ private val items: ThreadLocal<RecursionTracker<Any>> = ThreadLocal.withInitial {
+ RecursionTracker<Any> {
+ L.d("found recursive type, canceling resolution: %s", it)
+ }
+ }
+
+ /**
+ * Visits the given [referenceObject].
+ * If it is not in the stack, calls [process], if it is in the stack, calls [onRecursionDetected].
+ */
+ fun <T : Any, R> visit(referenceObject: T, process: (T) -> R, onRecursionDetected: (T) -> R): R {
+ if (!items.get().pushIfNew(referenceObject)) {
+ return onRecursionDetected(referenceObject)
+ }
+ try {
+ return process(referenceObject)
+ } finally {
+ items.get().popAndCheck(referenceObject)
+ }
+ }
+} \ No newline at end of file
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationTypeUtil.java b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationTypeUtil.java
index 1775d475..a4195e7a 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationTypeUtil.java
+++ b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationTypeUtil.java
@@ -18,6 +18,7 @@ package android.databinding.tool.reflection.annotation;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelMethod;
+import android.databinding.tool.reflection.RecursiveResolutionStack;
import android.databinding.tool.reflection.TypeUtil;
import java.util.List;
@@ -39,6 +40,8 @@ import javax.lang.model.type.WildcardType;
public class AnnotationTypeUtil extends TypeUtil {
javax.lang.model.util.Types mTypes;
+ // used to avoid recursion in toJava
+ private RecursiveResolutionStack mToJavaResolutionStack = new RecursiveResolutionStack();
public AnnotationTypeUtil(
AnnotationAnalyzer annotationAnalyzer) {
@@ -122,51 +125,65 @@ public class AnnotationTypeUtil extends TypeUtil {
* "java.util.Set&lt;java.lang.String&gt;"
*/
public String toJava(TypeMirror typeMirror) {
- switch (typeMirror.getKind()) {
- case BOOLEAN:
- return "boolean";
- case BYTE:
- return "byte";
- case SHORT:
- return "short";
- case INT:
- return "int";
- case LONG:
- return "long";
- case CHAR:
- return "char";
- case FLOAT:
- return "float";
- case DOUBLE:
- return "double";
- case VOID:
- return "void";
- case NULL:
- return "null";
- case ARRAY:
- return toJava((ArrayType) typeMirror);
- case DECLARED:
- return toJava((DeclaredType) typeMirror);
- case TYPEVAR:
- return toJava((TypeVariable) typeMirror);
- case WILDCARD:
- return toJava((WildcardType) typeMirror);
- case NONE:
- case PACKAGE:
- return toJava(mTypes.asElement(typeMirror));
- case EXECUTABLE:
- return toJava((ExecutableType) typeMirror);
- case UNION:
- return toJava((UnionType) typeMirror);
- case INTERSECTION:
- return toJava((IntersectionType) typeMirror);
- case ERROR:
- return mTypes.asElement(typeMirror).getSimpleName().toString();
- case OTHER:
- throw new IllegalArgumentException(
- "Unexpected TypeMirror kind " + typeMirror.getKind() + ": " + typeMirror);
- }
- throw new AssertionError(typeMirror.getKind());
+ return mToJavaResolutionStack.visit(
+ typeMirror,
+ current -> {
+ switch (current.getKind()) {
+ case BOOLEAN:
+ return "boolean";
+ case BYTE:
+ return "byte";
+ case SHORT:
+ return "short";
+ case INT:
+ return "int";
+ case LONG:
+ return "long";
+ case CHAR:
+ return "char";
+ case FLOAT:
+ return "float";
+ case DOUBLE:
+ return "double";
+ case VOID:
+ return "void";
+ case NULL:
+ return "null";
+ case ARRAY:
+ return toJava((ArrayType) current);
+ case DECLARED:
+ return toJava((DeclaredType) current);
+ case TYPEVAR:
+ return toJava((TypeVariable) current);
+ case WILDCARD:
+ return toJava((WildcardType) current);
+ case NONE:
+ case PACKAGE:
+ return toJava(mTypes.asElement(current));
+ case EXECUTABLE:
+ return toJava((ExecutableType) current);
+ case UNION:
+ return toJava((UnionType) current);
+ case INTERSECTION:
+ return toJava((IntersectionType) current);
+ case ERROR:
+ return mTypes.asElement(current).getSimpleName().toString();
+ case OTHER:
+ throw new IllegalArgumentException(
+ "Unexpected TypeMirror kind " + current.getKind() + ": " + current);
+ }
+ throw new AssertionError(current.getKind());
+ },
+ recursed -> {
+ if (recursed instanceof WildcardType) {
+ return ((WildcardType) recursed).getExtendsBound().toString();
+ } if (recursed instanceof TypeVariable) {
+ return "?";
+ } else {
+ return recursed.toString();
+ }
+ }
+ );
}
private String toJava(ArrayType arrayType) {
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
index f39df235..e9f8eab2 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
@@ -94,11 +94,11 @@ public class ErrorMessages {
"Expected: %d\n" +
"Found: %d";
- public static final String OBSERVABLE_FIELD_GET =
- "The call to 'get' is unnecessary for Observable field '%s' and should be removed";
-
- public static final String LIVEDATA_FIELD_GETVALUE =
- "The call to 'getValue' is unnecessary for LiveData field '%s' and should be removed";
+ public static final String RECURSIVE_OBSERVABLE =
+ "Observable fields (LiveData, Observable etc) cannot contain a value type of themselves: %s .\n" +
+ "\n" +
+ "This would create a situation where data binding would need to unwrap an observable indefinitely." +
+ "(e.g. unwrapping a class like `Foo extends Observable<Foo>` would result into another `Foo`)";
public static final String DUPLICATE_VIEW_OR_INCLUDE_ID =
"<%s id='%s'> conflicts with another tag that has the same ID";
diff --git a/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java b/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java
index a8d21613..49b676eb 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java
@@ -79,7 +79,7 @@ public class XmlEditor {
List<? extends ElementContext> layoutNodes =
excludeNodesByName("data", childrenOfRoot);
if (layoutNodes.size() != 1) {
- L.e("Only one layout element and one data element are allowed. %s has %d",
+ L.e("Only one layout element with 1 view child is allowed. %s has %d",
f.getAbsolutePath(), layoutNodes.size());
}
diff --git a/extensions-support/gradle/wrapper/gradle-wrapper.properties b/extensions-support/gradle/wrapper/gradle-wrapper.properties
index b8041665..1e6a306a 100644
--- a/extensions-support/gradle/wrapper/gradle-wrapper.properties
+++ b/extensions-support/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=../../../../external/gradle/gradle-5.5-bin.zip
+distributionUrl=../../../../external/gradle/gradle-5.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml b/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml
index 6a02a9f2..9153bc8a 100644
--- a/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml
+++ b/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml
@@ -11,16 +11,13 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="android.databinding.testapp">
- <uses-sdk android:minSdkVersion="14" tools:overrideLibrary="android.support.test,
- android.app, android.support.test.rule, android.support.test.espresso,
- android.support.test.espresso.idling"/>
- <application android:allowBackup="true">
- <activity android:name=".TestActivity"
- android:screenOrientation="portrait"/>
- <activity android:name=".LandscapeActivity"
- android:screenOrientation="landscape"/>
- </application>
+ xmlns:tools="http://schemas.android.com/tools"
+ package="android.databinding.testapp">
+ <uses-sdk
+ android:minSdkVersion="14"
+ tools:overrideLibrary="android.support.test,
+ android.app, android.support.test.rule, android.support.test.espresso,
+ android.support.test.espresso.idling" />
+ <application android:allowBackup="true"/>
</manifest>
diff --git a/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/RecursiveLayoutTest.java b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/RecursiveLayoutTest.java
new file mode 100644
index 00000000..0dfff7c0
--- /dev/null
+++ b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/RecursiveLayoutTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 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 android.databinding.testapp;
+
+import android.databinding.testapp.databinding.RecursiveLayoutBinding;
+import android.databinding.testapp.vo.RecursiveClass;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@RunWith(AndroidJUnit4.class)
+public class RecursiveLayoutTest extends BaseDataBinderTest<RecursiveLayoutBinding> {
+ public RecursiveLayoutTest() {
+ super(RecursiveLayoutBinding.class);
+ }
+
+ @Test
+ @UiThreadTest
+ public void runRecursiveTest() {
+ initBinder();
+ RecursiveClass recursiveClass = new RecursiveClass();
+ recursiveClass.text = "foo";
+
+ mBinder.setRecursiveClass(recursiveClass);
+ mBinder.executePendingBindings();
+ assertThat(
+ mBinder.recursiveClassText.getText().toString(),
+ is("foo")
+ );
+ assertThat(
+ mBinder.recursiveClassViaAdapterText.getText().toString(),
+ is("foo foo")
+ );
+ }
+}
diff --git a/integration-tests/TestApp/app/src/main/AndroidManifest.xml b/integration-tests/TestApp/app/src/main/AndroidManifest.xml
index 10b5ec55..201c0b23 100644
--- a/integration-tests/TestApp/app/src/main/AndroidManifest.xml
+++ b/integration-tests/TestApp/app/src/main/AndroidManifest.xml
@@ -12,15 +12,19 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.databinding.testapp">
+ package="android.databinding.testapp">
- <application android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- >
- <activity android:name=".TestActivity"
- android:screenOrientation="portrait"/>
- <activity android:name=".LandscapeActivity"
- android:screenOrientation="landscape"/>
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher">
+ <activity
+ android:name=".TestActivity"
+ android:screenOrientation="portrait"
+ android:theme="@style/noAnimTheme" />
+ <activity
+ android:name=".LandscapeActivity"
+ android:screenOrientation="landscape"
+ android:theme="@style/noAnimTheme"/>
</application>
</manifest>
diff --git a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/RecursiveAdapter.java b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/RecursiveAdapter.java
new file mode 100644
index 00000000..d4147fee
--- /dev/null
+++ b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/RecursiveAdapter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 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 android.databinding.testapp.adapter;
+
+import android.databinding.testapp.vo.RecursiveClass;
+import android.widget.TextView;
+
+import androidx.databinding.BindingAdapter;
+
+public class RecursiveAdapter {
+ @BindingAdapter("recursive")
+ public static void setRecursive(
+ TextView textView,
+ RecursiveClass recursive
+ ) {
+ textView.setText(recursive.text + " " + recursive.text);
+ }
+}
diff --git a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/RecursiveClass.java b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/RecursiveClass.java
new file mode 100644
index 00000000..d5d26f00
--- /dev/null
+++ b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/RecursiveClass.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 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 android.databinding.testapp.vo;
+
+public class RecursiveClass<T extends RecursiveClass<T>> {
+ public String text;
+}
diff --git a/integration-tests/TestApp/app/src/main/res/layout/recursive_layout.xml b/integration-tests/TestApp/app/src/main/res/layout/recursive_layout.xml
new file mode 100644
index 00000000..0b4025f9
--- /dev/null
+++ b/integration-tests/TestApp/app/src/main/res/layout/recursive_layout.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <data>
+ <variable name="recursiveClass" type="android.databinding.testapp.vo.RecursiveClass" />
+ </data>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/recursiveClassText"
+ android:text="@{recursiveClass.text}"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ <TextView
+ android:id="@+id/recursiveClassViaAdapterText"
+ app:recursive="@{recursiveClass}"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </LinearLayout>
+</layout> \ No newline at end of file
diff --git a/integration-tests/TestApp/app/src/main/res/values/styles.xml b/integration-tests/TestApp/app/src/main/res/values/styles.xml
index c0d54711..6f0b9d1d 100644
--- a/integration-tests/TestApp/app/src/main/res/values/styles.xml
+++ b/integration-tests/TestApp/app/src/main/res/values/styles.xml
@@ -14,8 +14,11 @@
<resources>
<!-- Base application theme. -->
- <style name="AppTheme" parent="android:Theme.Holo">
+ <style name="AppTheme" parent="android:Theme.Light">
<!-- Customize your theme here. -->
</style>
+ <style name="noAnimTheme" parent="AppTheme">
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
</resources>