diff options
author | Yigit Boyar <yboyar@google.com> | 2019-10-03 16:12:46 -0700 |
---|---|---|
committer | TreeHugger Robot <treehugger-gerrit@google.com> | 2019-10-08 21:10:34 +0000 |
commit | 103e4e323372565802ecb09ee689b77f43acc92e (patch) | |
tree | cb4eacc7ccd6cc6108ffa63aa16eae72561cad2a | |
parent | bff339917a58bccd30775d866fe12f67af166682 (diff) | |
download | data-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
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<java.lang.String>" */ 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> |