summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Mount <mount@google.com>2015-09-17 07:42:41 -0700
committerGeorge Mount <mount@google.com>2016-01-13 15:44:27 -0800
commitd3f2b9229472c9dae9bf4ae8b3e2d653b5653b01 (patch)
tree009fa87e9bb7c887b3466e84491868a4f9e29a0b
parent9e6805b0ab84967da017e48c5e8284d3263dae35 (diff)
downloaddata-binding-d3f2b9229472c9dae9bf4ae8b3e2d653b5653b01.tar.gz
Two-way binding
Bug 1474349 Bug 22460238 This adds two-way data binding for those attributes on Views that also have event listeners for them. General use is also supported, but event listeners are required to notify when those properties change. Change-Id: Iedc66a604257930265f9d661f69658a0a0c3208b
-rwxr-xr-x.idea/codeStyleSettings.xml15
-rw-r--r--baseLibrary/src/main/java/android/databinding/BindingMethod.java4
-rw-r--r--baseLibrary/src/main/java/android/databinding/InverseBindingAdapter.java91
-rw-r--r--baseLibrary/src/main/java/android/databinding/InverseBindingListener.java76
-rw-r--r--baseLibrary/src/main/java/android/databinding/InverseBindingMethod.java98
-rw-r--r--baseLibrary/src/main/java/android/databinding/InverseBindingMethods.java28
-rw-r--r--compiler/src/main/java/android/databinding/annotationprocessor/ProcessMethodAdapters.java150
-rw-r--r--compiler/src/main/java/android/databinding/tool/Binding.java15
-rw-r--r--compiler/src/main/java/android/databinding/tool/BindingTarget.java49
-rw-r--r--compiler/src/main/java/android/databinding/tool/InverseBinding.java180
-rw-r--r--compiler/src/main/java/android/databinding/tool/LayoutBinder.java48
-rw-r--r--compiler/src/main/java/android/databinding/tool/MergedBinding.java4
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java7
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/BitShiftExpr.java11
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/BracketExpr.java22
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/BuiltInVariableExpr.java15
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/CastExpr.java17
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ComparisonExpr.java11
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/Expr.java54
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ExprModel.java29
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java138
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/GroupExpr.java15
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/IdentifierExpr.java18
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/InstanceOfExpr.java9
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ListenerExpr.java7
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/MathExpr.java80
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java19
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java8
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/StaticIdentifierExpr.java12
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/SymbolExpr.java7
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/TernaryExpr.java33
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/TwoWayListenerExpr.java63
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/UnaryExpr.java16
-rw-r--r--compiler/src/main/java/android/databinding/tool/expr/ViewFieldExpr.java56
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/Callable.java10
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java62
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationAnalyzer.java12
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java54
-rw-r--r--compiler/src/main/java/android/databinding/tool/store/SetterStore.java485
-rw-r--r--compiler/src/main/kotlin/android/databinding/tool/writer/KCode.kt2
-rw-r--r--compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt49
-rw-r--r--compiler/src/test/java/android/databinding/tool/LayoutBinderTest.java12
-rw-r--r--compiler/src/test/java/android/databinding/tool/expr/ExprModelTest.java25
-rw-r--r--compiler/src/test/java/android/databinding/tool/expr/ExprTest.java20
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java6
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java42
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java18
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/util/L.java40
-rw-r--r--compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java42
49 files changed, 2062 insertions, 222 deletions
diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index 67bbdc43..bf5f929e 100755
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -13,7 +13,7 @@
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
- <package name="" withSubpackages="true" static="true" />
+ <package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
@@ -23,13 +23,13 @@
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
- <package name="android" withSubpackages="true" static="false" />
+ <package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
- <package name="" withSubpackages="true" static="false" />
+ <package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
@@ -83,6 +83,15 @@
</value>
</option>
</GroovyCodeStyleSettings>
+ <JetCodeStyleSettings>
+ <option name="PACKAGES_TO_USE_STAR_IMPORTS">
+ <value>
+ <package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
+ </value>
+ </option>
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
+ </JetCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
diff --git a/baseLibrary/src/main/java/android/databinding/BindingMethod.java b/baseLibrary/src/main/java/android/databinding/BindingMethod.java
index 3585c0ce..d96b930b 100644
--- a/baseLibrary/src/main/java/android/databinding/BindingMethod.java
+++ b/baseLibrary/src/main/java/android/databinding/BindingMethod.java
@@ -15,11 +15,15 @@
*/
package android.databinding;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
/**
* Used within an {@link BindingMethods} annotation to describe a renaming of an attribute to
* the setter used to set that attribute. By default, an attribute attr will be associated with
* setter setAttr.
*/
+@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
/**
diff --git a/baseLibrary/src/main/java/android/databinding/InverseBindingAdapter.java b/baseLibrary/src/main/java/android/databinding/InverseBindingAdapter.java
new file mode 100644
index 00000000..f3e67d23
--- /dev/null
+++ b/baseLibrary/src/main/java/android/databinding/InverseBindingAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * InverseBindingAdapter is associated with a method used to retrieve the value for a View
+ * when setting values gathered from the View. This is similar to {@link BindingAdapter}s:
+ * <pre>
+ * &commat;InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
+ * public static String captureTextValue(TextView view, CharSequence originalValue) {
+ * CharSequence newValue = view.getText();
+ * CharSequence oldValue = value.get();
+ * if (oldValue == null) {
+ * value.set(newValue);
+ * } else if (!contentEquals(newValue, oldValue)) {
+ * value.set(newValue);
+ * }
+ * }
+ * </pre>
+ * <p>
+ * The default value for event is the attribute name suffixed with "AttrChanged". In the
+ * above example, the default value would have been <code>android:textAttrChanged</code> even
+ * if it wasn't provided.
+ * <p>
+ * The event attribute is used to notify the data binding system that the value has changed.
+ * The developer will typically create a {@link BindingAdapter} to assign the event. For example:
+ * <p>
+ * <pre>
+ * @commat;BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
+ * "android:afterTextChanged", "android:textAttrChanged"},
+ * requireAll = false)
+ * public static void setTextWatcher(TextView view, final BeforeTextChanged before,
+ * final OnTextChanged on, final AfterTextChanged after,
+ * final InverseBindingListener textAttrChanged) {
+ * TextWatcher newValue = new TextWatcher() {
+ * ...
+ * @commat;Override
+ * public void onTextChanged(CharSequence s, int start, int before, int count) {
+ * if (on != null) {
+ * on.onTextChanged(s, start, before, count);
+ * }
+ * if (textAttrChanged != null) {
+ * textAttrChanged.onChange();
+ * }
+ * }
+ * }
+ * TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
+ * if (oldValue != null) {
+ * view.removeTextChangedListener(oldValue);
+ * }
+ * view.addTextChangedListener(newValue);
+ * }
+ * </pre>
+ * <p>
+ * Like <code>BindingAdapter</code>s, InverseBindingAdapter methods may also take
+ * {@link DataBindingComponent} as the first parameter and may be an instance method with the
+ * instance retrieved from the <code>DataBindingComponent</code>.
+ *
+ * @see DataBindingUtil#setDefaultComponent(DataBindingComponent)
+ * @see InverseBindingMethod
+ */
+@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
+public @interface InverseBindingAdapter {
+
+ /**
+ * The attribute that the value is to be retrieved for.
+ */
+ String attribute();
+
+ /**
+ * The event used to trigger changes. This is used in {@link BindingAdapter}s for the
+ * data binding system to set the event listener when two-way binding is used.
+ */
+ String event() default "";
+}
diff --git a/baseLibrary/src/main/java/android/databinding/InverseBindingListener.java b/baseLibrary/src/main/java/android/databinding/InverseBindingListener.java
new file mode 100644
index 00000000..f6d76e0a
--- /dev/null
+++ b/baseLibrary/src/main/java/android/databinding/InverseBindingListener.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 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;
+
+/**
+ * A listener implemented by all two-way bindings to be notified when a triggering change happens.
+ * For example, when there is a two-way binding for android:text, an implementation of
+ * <code>InverseBindingListener</code> will be generated in the layout's binding class.
+ * <pre>
+ * private static class InverseListenerTextView implements InverseBindingListener {
+ * &commat;Override
+ * public void onChange() {
+ * mObj.setTextValue(mTextView.getText());
+ * }
+ * }
+ * </pre>
+ * <p>
+ * A {@link BindingAdapter} should be used to assign the event listener.
+ * For example, <code>android:onTextChanged</code> will need to trigger the event listener
+ * for the <code>android:text</code> attribute.
+ * <pre>
+ * &commat;InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
+ * public static void captureTextValue(TextView view, ObservableField&lt;CharSequence> value) {
+ * CharSequence newValue = view.getText();
+ * CharSequence oldValue = value.get();
+ * if (oldValue == null) {
+ * value.set(newValue);
+ * } else if (!contentEquals(newValue, oldValue)) {
+ * value.set(newValue);
+ * }
+ * }
+ * @commat;BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
+ * "android:afterTextChanged", "android:textAttrChanged"},
+ * requireAll = false)
+ * public static void setTextWatcher(TextView view, final BeforeTextChanged before,
+ * final OnTextChanged on, final AfterTextChanged after,
+ * final InverseBindingListener textAttrChanged) {
+ * TextWatcher newValue = new TextWatcher() {
+ * ...
+ * @commat;Override
+ * public void onTextChanged(CharSequence s, int start, int before, int count) {
+ * if (on != null) {
+ * on.onTextChanged(s, start, before, count);
+ * }
+ * if (textAttrChanged != null) {
+ * textAttrChanged.onChange();
+ * }
+ * }
+ * }
+ * TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
+ * if (oldValue != null) {
+ * view.removeTextChangedListener(oldValue);
+ * }
+ * view.addTextChangedListener(newValue);
+ * }
+ * </pre>
+ */
+public interface InverseBindingListener {
+ /**
+ * Notifies the data binding system that the attribute value has changed.
+ */
+ void onChange();
+}
diff --git a/baseLibrary/src/main/java/android/databinding/InverseBindingMethod.java b/baseLibrary/src/main/java/android/databinding/InverseBindingMethod.java
new file mode 100644
index 00000000..c0cf99c7
--- /dev/null
+++ b/baseLibrary/src/main/java/android/databinding/InverseBindingMethod.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * InverseBindingMethod is used to identify how to listen for changes to a View property and which
+ * getter method to call. InverseBindingMethod should be associated with any class as part of
+ * {@link InverseBindingMethods}.
+ * <p>
+ * <pre>
+ * &commat;InverseBindingMethods({&commat;InverseBindingMethod(
+ * type = android.widget.TextView.class,
+ * attribute = "android:text",
+ * event = "android:textAttrChanged",
+ * method = "getText")})
+ * public class MyTextViewBindingAdapters { ... }
+ * </pre>
+ * <p>
+ * <code>method</code> is optional. If it isn't provided, the attribute name is used to
+ * find the method name, either prefixing with "is" or "get". For the attribute
+ * <code>android:text</code>, data binding will search for a
+ * <code>public CharSequence getText()</code> method on {@link android.widget.TextView}.
+ * <p>
+ * <code>event</code> is optional. If it isn't provided, the event name is assigned the
+ * attribute name suffixed with <code>AttrChanged</code>. For the <code>android:text</code>
+ * attribute, the default event name would be <code>android:textAttrChanged</code>. The event
+ * should be set using a {@link BindingAdapter}. For example:
+ * <pre>
+ * @commat;BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
+ * "android:afterTextChanged", "android:textAttrChanged"},
+ * requireAll = false)
+ * public static void setTextWatcher(TextView view, final BeforeTextChanged before,
+ * final OnTextChanged on, final AfterTextChanged after,
+ * final InverseBindingListener textAttrChanged) {
+ * TextWatcher newValue = new TextWatcher() {
+ * ...
+ * @commat;Override
+ * public void onTextChanged(CharSequence s, int start, int before, int count) {
+ * if (on != null) {
+ * on.onTextChanged(s, start, before, count);
+ * }
+ * if (textAttrChanged != null) {
+ * textAttrChanged.onChange();
+ * }
+ * }
+ * }
+ * TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
+ * if (oldValue != null) {
+ * view.removeTextChangedListener(oldValue);
+ * }
+ * view.addTextChangedListener(newValue);
+ * }
+ * </pre>
+ *
+ * @see InverseBindingAdapter
+ * @see InverseBindingListener
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+public @interface InverseBindingMethod {
+
+ /**
+ * The View type that is associated with the attribute.
+ */
+ Class type();
+
+ /**
+ * The attribute that supports two-way binding.
+ */
+ String attribute();
+
+ /**
+ * The event used to notify the data binding system that the attribute value has changed.
+ * Defaults to attribute() + "AttrChanged"
+ */
+ String event() default "";
+
+ /**
+ * The getter method to retrieve the attribute value from the View. The default is
+ * the bean method name based on the attribute name.
+ */
+ String method() default "";
+}
diff --git a/baseLibrary/src/main/java/android/databinding/InverseBindingMethods.java b/baseLibrary/src/main/java/android/databinding/InverseBindingMethods.java
new file mode 100644
index 00000000..29e2374b
--- /dev/null
+++ b/baseLibrary/src/main/java/android/databinding/InverseBindingMethods.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Used to enumerate attribute, getter, and event association. The value is an array of
+ * {@link InverseBindingMethod}s.
+ */
+@Target(ElementType.TYPE)
+public @interface InverseBindingMethods {
+ InverseBindingMethod[] value();
+}
diff --git a/compiler/src/main/java/android/databinding/annotationprocessor/ProcessMethodAdapters.java b/compiler/src/main/java/android/databinding/annotationprocessor/ProcessMethodAdapters.java
index 272b5d18..36c4dd88 100644
--- a/compiler/src/main/java/android/databinding/annotationprocessor/ProcessMethodAdapters.java
+++ b/compiler/src/main/java/android/databinding/annotationprocessor/ProcessMethodAdapters.java
@@ -20,6 +20,9 @@ import android.databinding.BindingBuildInfo;
import android.databinding.BindingConversion;
import android.databinding.BindingMethod;
import android.databinding.BindingMethods;
+import android.databinding.InverseBindingAdapter;
+import android.databinding.InverseBindingMethod;
+import android.databinding.InverseBindingMethods;
import android.databinding.Untaggable;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.store.SetterStore;
@@ -43,9 +46,10 @@ import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
-import javax.tools.Diagnostic;
public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
+ private final static String INVERSE_BINDING_EVENT_ATTR_SUFFIX = "AttrChanged";
+
public ProcessMethodAdapters() {
}
@@ -60,9 +64,11 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
clearIncrementalClasses(roundEnv, store);
addBindingAdapters(roundEnv, processingEnvironment, store);
- addRenamed(roundEnv, processingEnvironment, store);
- addConversions(roundEnv, processingEnvironment, store);
- addUntaggable(roundEnv, processingEnvironment, store);
+ addRenamed(roundEnv, store);
+ addConversions(roundEnv, store);
+ addUntaggable(roundEnv, store);
+ addInverseAdapters(roundEnv, processingEnvironment, store);
+ addInverseMethods(roundEnv, store);
try {
store.write(buildInfo.modulePackage(), processingEnvironment);
@@ -84,7 +90,7 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
.getElementsAnnotatedWith(roundEnv, BindingAdapter.class)) {
if (element.getKind() != ElementKind.METHOD ||
!element.getModifiers().contains(Modifier.PUBLIC)) {
- L.e("@BindingAdapter on invalid element: %s", element);
+ L.e(element, "@BindingAdapter on invalid element: %s", element);
continue;
}
BindingAdapter bindingAdapter = element.getAnnotation(BindingAdapter.class);
@@ -92,7 +98,8 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
ExecutableElement executableElement = (ExecutableElement) element;
List<? extends VariableElement> parameters = executableElement.getParameters();
if (bindingAdapter.value().length == 0) {
- L.e("@BindingAdapter requires at least one attribute. %s", element);
+ L.e(element, "@BindingAdapter requires at least one attribute. %s",
+ element);
continue;
}
@@ -107,9 +114,9 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
for (int i = startIndex; i < numAttributes + startIndex; i++) {
if (!typeUtils.isSameType(parameters.get(i).asType(),
parameters.get(i + numAttributes).asType())) {
- L.e("BindingAdapter %s: old values should be followed by new values. " +
- "Parameter %d must be the same type as parameter %d.",
- executableElement, i + 1, i + numAttributes + 1);
+ L.e(executableElement, "BindingAdapter %s: old values should be followed " +
+ "by new values. Parameter %d must be the same type as parameter " +
+ "%d.", executableElement, i + 1, i + numAttributes + 1);
hasParameterError = true;
break;
}
@@ -118,16 +125,16 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
continue;
}
} else if (numAdditionalArgs != numAttributes) {
- L.e("@BindingAdapter %s has %d attributes and %d value parameters. There should " +
- "be %d or %d value parameters.", executableElement, numAttributes,
- numAdditionalArgs, numAttributes, numAttributes * 2);
+ L.e(element, "@BindingAdapter %s has %d attributes and %d value " +
+ "parameters. There should be %d or %d value parameters.",
+ executableElement, numAttributes, numAdditionalArgs, numAttributes,
+ numAttributes * 2);
continue;
}
- warnAttributeNamespaces(bindingAdapter.value());
+ warnAttributeNamespaces(element, bindingAdapter.value());
try {
if (numAttributes == 1) {
final String attribute = bindingAdapter.value()[0];
- L.d("------------------ @BindingAdapter for %s", element);
store.addBindingAdapter(processingEnv, attribute, executableElement,
takesComponent);
} else {
@@ -135,7 +142,7 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
executableElement, takesComponent, bindingAdapter.requireAll());
}
} catch (IllegalArgumentException e) {
- L.e(e, "@BindingAdapter for duplicate View and parameter type: %s", element);
+ L.e(element, "@BindingAdapter for duplicate View and parameter type: %s", element);
}
}
}
@@ -158,8 +165,8 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
TypeMirror viewStubProxy = elementUtils.
getTypeElement("android.databinding.ViewStubProxy").asType();
if (!typeUtils.isAssignable(parameter1, viewStubProxy)) {
- L.e("@BindingAdapter %s is applied to a method that has two parameters, the " +
- "first must be a View type", executableElement);
+ L.e(executableElement, "@BindingAdapter %s is applied to a method that has two " +
+ "parameters, the first must be a View type", executableElement);
}
return false;
}
@@ -167,27 +174,27 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
if (typeUtils.isAssignable(parameter2, viewElement)) {
return true; // second parameter is a View
}
- L.e("@BindingAdapter %s is applied to a method that doesn't take a View subclass as the " +
- "first or second parameter. When a BindingAdapter uses a DataBindingComponent, " +
- "the component parameter is first and the View parameter is second, otherwise " +
- "the View parameter is first.", executableElement);
+ L.e(executableElement, "@BindingAdapter %s is applied to a method that doesn't take a " +
+ "View subclass as the first or second parameter. When a BindingAdapter uses a " +
+ "DataBindingComponent, the component parameter is first and the View " +
+ "parameter is second, otherwise the View parameter is first.",
+ executableElement);
return false;
}
- private static void warnAttributeNamespace(String attribute) {
+ private static void warnAttributeNamespace(Element element, String attribute) {
if (attribute.contains(":") && !attribute.startsWith("android:")) {
- L.w("Application namespace for attribute %s will be ignored.", attribute);
+ L.w(element, "Application namespace for attribute %s will be ignored.", attribute);
}
}
- private static void warnAttributeNamespaces(String[] attributes) {
+ private static void warnAttributeNamespaces(Element element, String[] attributes) {
for (String attribute : attributes) {
- warnAttributeNamespace(attribute);
+ warnAttributeNamespace(element, attribute);
}
}
- private void addRenamed(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv,
- SetterStore store) {
+ private void addRenamed(RoundEnvironment roundEnv, SetterStore store) {
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingMethods.class)) {
BindingMethods bindingMethods = element.getAnnotation(BindingMethods.class);
@@ -195,7 +202,7 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
for (BindingMethod bindingMethod : bindingMethods.value()) {
final String attribute = bindingMethod.attribute();
final String method = bindingMethod.method();
- warnAttributeNamespace(attribute);
+ warnAttributeNamespace(element, attribute);
String type;
try {
type = bindingMethod.type().getCanonicalName();
@@ -207,38 +214,97 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
}
}
- private void addConversions(RoundEnvironment roundEnv,
- ProcessingEnvironment processingEnv, SetterStore store) {
+ private void addConversions(RoundEnvironment roundEnv, SetterStore store) {
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingConversion.class)) {
if (element.getKind() != ElementKind.METHOD ||
!element.getModifiers().contains(Modifier.STATIC) ||
!element.getModifiers().contains(Modifier.PUBLIC)) {
- processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
- "@BindingConversion is only allowed on public static methods: " + element);
+ L.e(element, "@BindingConversion is only allowed on public static methods %s",
+ element);
continue;
}
ExecutableElement executableElement = (ExecutableElement) element;
if (executableElement.getParameters().size() != 1) {
- processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
- "@BindingConversion method should have one parameter: " + element);
+ L.e(element, "@BindingConversion method should have one parameter %s", element);
continue;
}
if (executableElement.getReturnType().getKind() == TypeKind.VOID) {
- processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
- "@BindingConversion method must return a value: " + element);
+ L.e(element, "@BindingConversion method must return a value %s", element);
continue;
}
- processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
- "added conversion: " + element);
store.addConversionMethod(executableElement);
}
}
- private void addUntaggable(RoundEnvironment roundEnv,
+ private void addInverseAdapters(RoundEnvironment roundEnv,
ProcessingEnvironment processingEnv, SetterStore store) {
- for (Element element : AnnotationUtil.getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
+ for (Element element : AnnotationUtil
+ .getElementsAnnotatedWith(roundEnv, InverseBindingAdapter.class)) {
+ if (!element.getModifiers().contains(Modifier.PUBLIC)) {
+ L.e(element, "@InverseBindingAdapter must be associated with a public method");
+ continue;
+ }
+ ExecutableElement executableElement = (ExecutableElement) element;
+ if (executableElement.getReturnType().getKind() == TypeKind.VOID) {
+ L.e(element, "@InverseBindingAdapter must have a non-void return type");
+ continue;
+ }
+ final InverseBindingAdapter inverseBindingAdapter =
+ executableElement.getAnnotation(InverseBindingAdapter.class);
+ final String attribute = inverseBindingAdapter.attribute();
+ warnAttributeNamespace(element, attribute);
+ final String event = inverseBindingAdapter.event().isEmpty()
+ ? inverseBindingAdapter.attribute() + INVERSE_BINDING_EVENT_ATTR_SUFFIX
+ : inverseBindingAdapter.event();
+ warnAttributeNamespace(element, event);
+ final boolean takesComponent = takesComponent(executableElement, processingEnv);
+ final int expectedArgs = takesComponent ? 2 : 1;
+ final int numParameters = executableElement.getParameters().size();
+ if (numParameters != expectedArgs) {
+ L.e(element, "@InverseBindingAdapter %s takes %s parameters, but %s parameters " +
+ "were expected", element, numParameters, expectedArgs);
+ continue;
+ }
+ try {
+ store.addInverseAdapter(processingEnv, attribute, event, executableElement,
+ takesComponent);
+ } catch (IllegalArgumentException e) {
+ L.e(element, "@InverseBindingAdapter for duplicate View and parameter type: %s",
+ element);
+ }
+ }
+ }
+
+ private void addInverseMethods(RoundEnvironment roundEnv, SetterStore store) {
+ for (Element element : AnnotationUtil
+ .getElementsAnnotatedWith(roundEnv, InverseBindingMethods.class)) {
+ InverseBindingMethods bindingMethods =
+ element.getAnnotation(InverseBindingMethods.class);
+
+ for (InverseBindingMethod bindingMethod : bindingMethods.value()) {
+ final String attribute = bindingMethod.attribute();
+ final String method = bindingMethod.method();
+ final String event = bindingMethod.event().isEmpty()
+ ? bindingMethod.attribute() + INVERSE_BINDING_EVENT_ATTR_SUFFIX
+ : bindingMethod.event();
+ warnAttributeNamespace(element, attribute);
+ warnAttributeNamespace(element, event);
+ String type;
+ try {
+ type = bindingMethod.type().getCanonicalName();
+ } catch (MirroredTypeException e) {
+ type = e.getTypeMirror().toString();
+ }
+ store.addInverseMethod(attribute, event, type, method, (TypeElement) element);
+ }
+ }
+ }
+
+ private void addUntaggable(RoundEnvironment roundEnv, SetterStore store) {
+ for (Element element : AnnotationUtil.
+ getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
Untaggable untaggable = element.getAnnotation(Untaggable.class);
store.addUntaggableTypes(untaggable.value(), (TypeElement) element);
}
@@ -261,10 +327,10 @@ public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
classes.add(((TypeElement) element.getEnclosingElement()).getQualifiedName().
toString());
}
- for (Element element : AnnotationUtil.getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
+ for (Element element : AnnotationUtil.
+ getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
classes.add(((TypeElement) element).getQualifiedName().toString());
}
store.clear(classes);
}
-
}
diff --git a/compiler/src/main/java/android/databinding/tool/Binding.java b/compiler/src/main/java/android/databinding/tool/Binding.java
index 5aa89803..aba6b3eb 100644
--- a/compiler/src/main/java/android/databinding/tool/Binding.java
+++ b/compiler/src/main/java/android/databinding/tool/Binding.java
@@ -24,6 +24,7 @@ import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.store.Location;
import android.databinding.tool.store.SetterStore;
+import android.databinding.tool.store.SetterStore.BindingSetterCall;
import android.databinding.tool.store.SetterStore.SetterCall;
import android.databinding.tool.util.L;
import android.databinding.tool.writer.LayoutBinderWriterKt;
@@ -35,12 +36,17 @@ public class Binding implements LocationScopeProvider {
private final String mName;
private Expr mExpr;
private final BindingTarget mTarget;
- private SetterStore.SetterCall mSetterCall;
+ private BindingSetterCall mSetterCall;
public Binding(BindingTarget target, String name, Expr expr) {
+ this(target, name, expr, null);
+ }
+
+ public Binding(BindingTarget target, String name, Expr expr, BindingSetterCall setterCall) {
mTarget = target;
mName = name;
mExpr = expr;
+ mSetterCall = setterCall;
}
@Override
@@ -57,6 +63,13 @@ public class Binding implements LocationScopeProvider {
}
}
+ public void resolveTwoWayExpressions() {
+ Expr expr = mExpr.resolveTwoWayExpressions(null);
+ if (expr != mExpr) {
+ mExpr = expr;
+ }
+ }
+
private SetterStore.BindingSetterCall getSetterCall() {
if (mSetterCall == null) {
try {
diff --git a/compiler/src/main/java/android/databinding/tool/BindingTarget.java b/compiler/src/main/java/android/databinding/tool/BindingTarget.java
index 63fc4238..d8db559e 100644
--- a/compiler/src/main/java/android/databinding/tool/BindingTarget.java
+++ b/compiler/src/main/java/android/databinding/tool/BindingTarget.java
@@ -18,6 +18,7 @@ package android.databinding.tool;
import android.databinding.tool.expr.Expr;
import android.databinding.tool.expr.ExprModel;
+import android.databinding.tool.processing.ErrorMessages;
import android.databinding.tool.processing.Scope;
import android.databinding.tool.processing.scopes.LocationScopeProvider;
import android.databinding.tool.reflection.ModelAnalyzer;
@@ -25,6 +26,7 @@ import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.store.Location;
import android.databinding.tool.store.ResourceBundle;
import android.databinding.tool.store.SetterStore;
+import android.databinding.tool.store.SetterStore.BindingGetterCall;
import android.databinding.tool.util.L;
import android.databinding.tool.util.Preconditions;
@@ -35,12 +37,13 @@ import java.util.Map;
public class BindingTarget implements LocationScopeProvider {
List<Binding> mBindings = new ArrayList<Binding>();
+ List<InverseBinding> mInverseBindings = new ArrayList<InverseBinding>();
ExprModel mModel;
ModelClass mResolvedClass;
// if this target presents itself in multiple layout files with different view types,
// it receives an interface type and should use it in the getter instead.
- private ResourceBundle.BindingTargetBundle mBundle;
+ ResourceBundle.BindingTargetBundle mBundle;
public BindingTarget(ResourceBundle.BindingTargetBundle bundle) {
mBundle = bundle;
@@ -51,13 +54,38 @@ public class BindingTarget implements LocationScopeProvider {
}
public void addBinding(String name, Expr expr) {
+ if (SetterStore.get(ModelAnalyzer.getInstance()).isTwoWayEventAttribute(name)) {
+ L.e(ErrorMessages.TWO_WAY_EVENT_ATTRIBUTE, name);
+ }
mBindings.add(new Binding(this, name, expr));
+ if (expr.isTwoWay()) {
+ try {
+ Scope.enter(expr);
+ expr.assertIsInvertible();
+ final InverseBinding inverseBinding = new InverseBinding(this, name, expr);
+ mInverseBindings.add(inverseBinding);
+ mBindings.add(new Binding(this, inverseBinding.getEventAttribute(),
+ mModel.twoWayListenerExpr(inverseBinding),
+ inverseBinding.getEventSetter()));
+ } finally {
+ Scope.exit();
+ }
+ }
}
public String getInterfaceType() {
return mBundle.getInterfaceType() == null ? mBundle.getFullClassName() : mBundle.getInterfaceType();
}
+ public InverseBinding addInverseBinding(String name, BindingGetterCall call) {
+ final InverseBinding inverseBinding = new InverseBinding(this, name, null);
+ inverseBinding.setGetterCall(call);
+ mInverseBindings.add(inverseBinding);
+ mBindings.add(new Binding(this, inverseBinding.getEventAttribute(),
+ mModel.twoWayListenerExpr(inverseBinding)));
+ return inverseBinding;
+ }
+
@Override
public List<Location> provideScopeLocation() {
return mBundle.provideScopeLocation();
@@ -81,8 +109,13 @@ public class BindingTarget implements LocationScopeProvider {
public ModelClass getResolvedType() {
if (mResolvedClass == null) {
- mResolvedClass = ModelAnalyzer.getInstance().findClass(mBundle.getFullClassName(),
- mModel.getImports());
+ if (mBundle.isBinder()) {
+ mResolvedClass = ModelAnalyzer.getInstance().
+ findClass(ModelAnalyzer.VIEW_DATA_BINDING, mModel.getImports());
+ } else {
+ mResolvedClass = ModelAnalyzer.getInstance().findClass(mBundle.getFullClassName(),
+ mModel.getImports());
+ }
}
return mResolvedClass;
}
@@ -104,6 +137,10 @@ public class BindingTarget implements LocationScopeProvider {
return mBindings;
}
+ public List<InverseBinding> getInverseBindings() {
+ return mInverseBindings;
+ }
+
public ExprModel getModel() {
return mModel;
}
@@ -118,6 +155,12 @@ public class BindingTarget implements LocationScopeProvider {
}
}
+ public void resolveTwoWayExpressions() {
+ for (Binding binding : mBindings) {
+ binding.resolveTwoWayExpressions();
+ }
+ }
+
/**
* Called after BindingTarget is finalized.
* <p>
diff --git a/compiler/src/main/java/android/databinding/tool/InverseBinding.java b/compiler/src/main/java/android/databinding/tool/InverseBinding.java
new file mode 100644
index 00000000..e04be283
--- /dev/null
+++ b/compiler/src/main/java/android/databinding/tool/InverseBinding.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.databinding.tool.expr.Expr;
+import android.databinding.tool.expr.ExprModel;
+import android.databinding.tool.expr.FieldAccessExpr;
+import android.databinding.tool.processing.ErrorMessages;
+import android.databinding.tool.processing.Scope;
+import android.databinding.tool.processing.scopes.LocationScopeProvider;
+import android.databinding.tool.reflection.ModelAnalyzer;
+import android.databinding.tool.reflection.ModelClass;
+import android.databinding.tool.store.Location;
+import android.databinding.tool.store.SetterStore;
+import android.databinding.tool.store.SetterStore.BindingGetterCall;
+import android.databinding.tool.store.SetterStore.BindingSetterCall;
+import android.databinding.tool.util.L;
+import android.databinding.tool.util.Preconditions;
+import android.databinding.tool.writer.FlagSet;
+import android.databinding.tool.writer.KCode;
+import android.databinding.tool.writer.LayoutBinderWriterKt;
+
+import kotlin.jvm.functions.Function2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InverseBinding implements LocationScopeProvider {
+
+ private final String mName;
+ private final Expr mExpr;
+ private final BindingTarget mTarget;
+ private BindingGetterCall mGetterCall;
+ private final ArrayList<FieldAccessExpr> mChainedExpressions = new ArrayList<FieldAccessExpr>();
+
+ public InverseBinding(BindingTarget target, String name, Expr expr) {
+ mTarget = target;
+ mName = name;
+ mExpr = expr;
+ }
+
+ @Override
+ public List<Location> provideScopeLocation() {
+ if (mExpr != null) {
+ return mExpr.getLocations();
+ } else {
+ return mChainedExpressions.get(0).getLocations();
+ }
+ }
+
+ void setGetterCall(BindingGetterCall getterCall) {
+ mGetterCall = getterCall;
+ }
+
+ public void addChainedExpression(FieldAccessExpr expr) {
+ mChainedExpressions.add(expr);
+ }
+
+ public boolean isOnBinder() {
+ return mTarget.getResolvedType().isViewDataBinding();
+ }
+
+ private SetterStore.BindingGetterCall getGetterCall() {
+ if (mGetterCall == null) {
+ if (mExpr != null) {
+ mExpr.getResolvedType(); // force resolve of ObservableFields
+ }
+ try {
+ Scope.enter(mTarget);
+ Scope.enter(this);
+ resolveGetterCall();
+ if (mGetterCall == null) {
+ L.e(ErrorMessages.CANNOT_FIND_GETTER_CALL, mName,
+ mExpr == null ? "Unknown" : mExpr.getResolvedType(),
+ mTarget.getResolvedType());
+ }
+ } finally {
+ Scope.exit();
+ Scope.exit();
+ }
+ }
+ return mGetterCall;
+ }
+
+ private void resolveGetterCall() {
+ ModelClass viewType = mTarget.getResolvedType();
+ final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance());
+ final ModelClass resolvedType = mExpr == null ? null : mExpr.getResolvedType();
+ mGetterCall = setterStore.getGetterCall(mName, viewType, resolvedType,
+ getModel().getImports());
+ }
+
+ public BindingTarget getTarget() {
+ return mTarget;
+ }
+
+ public KCode toJavaCode(String bindingComponent, final FlagSet flagField) {
+ final String targetViewName = LayoutBinderWriterKt.getFieldName(getTarget());
+ KCode code = new KCode();
+ // A chained expression will have substituted its chained value for the expression
+ // unless the attribute has no expression. Therefore, chaining and expressions are
+ // mutually exclusive.
+ Preconditions.check((mExpr == null) != mChainedExpressions.isEmpty(),
+ "Chained expressions are only against unbound attributes.");
+ if (mExpr != null) {
+ code.app("", mExpr.toInverseCode(new KCode(getGetterCall().toJava(bindingComponent,
+ targetViewName))));
+ } else { // !mChainedExpressions.isEmpty())
+ final String fieldName = flagField.getLocalName();
+ FlagSet flagSet = new FlagSet();
+ for (FieldAccessExpr expr : mChainedExpressions) {
+ flagSet = flagSet.or(new FlagSet(expr.getId()));
+ }
+ final FlagSet allFlags = flagSet;
+ code.nl(new KCode("synchronized(this) {"));
+ code.tab(LayoutBinderWriterKt
+ .mapOr(flagField, flagSet, new Function2<String, Integer, KCode>() {
+ @Override
+ public KCode invoke(String suffix, Integer index) {
+ return new KCode(fieldName)
+ .app(suffix)
+ .app(" |= ")
+ .app(LayoutBinderWriterKt.binaryCode(allFlags, index))
+ .app(";");
+ }
+ }));
+ code.nl(new KCode("}"));
+ code.nl(new KCode("requestRebind()"));
+ }
+ return code;
+ }
+
+ public String getBindingAdapterInstanceClass() {
+ return getGetterCall().getBindingAdapterInstanceClass();
+ }
+
+ /**
+ * The min api level in which this binding should be executed.
+ * <p>
+ * This should be the minimum value among the dependencies of this binding.
+ */
+ public int getMinApi() {
+ final BindingGetterCall getterCall = getGetterCall();
+ return Math.max(getterCall.getMinApi(), getterCall.getEvent().getMinApi());
+ }
+
+ public BindingSetterCall getEventSetter() {
+ final BindingGetterCall getterCall = getGetterCall();
+ return getterCall.getEvent();
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getEventAttribute() {
+ return getGetterCall().getEventAttribute();
+ }
+
+ public ExprModel getModel() {
+ if (mExpr != null) {
+ return mExpr.getModel();
+ }
+ return mChainedExpressions.get(0).getModel();
+ }
+}
diff --git a/compiler/src/main/java/android/databinding/tool/LayoutBinder.java b/compiler/src/main/java/android/databinding/tool/LayoutBinder.java
index 755f21b3..7ed944a8 100644
--- a/compiler/src/main/java/android/databinding/tool/LayoutBinder.java
+++ b/compiler/src/main/java/android/databinding/tool/LayoutBinder.java
@@ -16,8 +16,6 @@
package android.databinding.tool;
-import org.antlr.v4.runtime.misc.Nullable;
-
import android.databinding.tool.expr.Dependency;
import android.databinding.tool.expr.Expr;
import android.databinding.tool.expr.ExprModel;
@@ -27,14 +25,18 @@ import android.databinding.tool.processing.scopes.FileScopeProvider;
import android.databinding.tool.store.Location;
import android.databinding.tool.store.ResourceBundle;
import android.databinding.tool.store.ResourceBundle.BindingTargetBundle;
+import android.databinding.tool.util.L;
import android.databinding.tool.util.Preconditions;
import android.databinding.tool.writer.LayoutBinderWriter;
import android.databinding.tool.writer.LayoutBinderWriterKt;
+import org.antlr.v4.runtime.misc.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
/**
@@ -171,42 +173,61 @@ public class LayoutBinder implements FileScopeProvider {
mBindingTargets = new ArrayList<BindingTarget>();
mBundle = layoutBundle;
mModulePackage = layoutBundle.getModulePackage();
+ HashSet<String> names = new HashSet<String>();
// copy over data.
- boolean addContext = true;
for (ResourceBundle.VariableDeclaration variable : mBundle.getVariables()) {
addVariable(variable.name, variable.type, variable.location, variable.declared);
- if ("context".equals(variable.name)) {
- addContext = false;
- }
+ names.add(variable.name);
}
for (ResourceBundle.NameTypeLocation userImport : mBundle.getImports()) {
mExprModel.addImport(userImport.name, userImport.type, userImport.location);
- if ("context".equals(userImport.name)) {
- addContext = false;
- }
+ names.add(userImport.name);
}
- if (addContext) {
+ if (!names.contains("context")) {
mExprModel.builtInVariable("context", "android.content.Context",
"getRoot().getContext()");
+ names.add("context");
}
for (String javaLangClass : sJavaLangClasses) {
mExprModel.addImport(javaLangClass, "java.lang." + javaLangClass, null);
}
+ // First resolve all the View fields
+ // Ensure there are no conflicts with variable names
for (BindingTargetBundle targetBundle : mBundle.getBindingTargetBundles()) {
try {
Scope.enter(targetBundle);
final BindingTarget bindingTarget = createBindingTarget(targetBundle);
- for (BindingTargetBundle.BindingBundle bindingBundle : targetBundle
+ if (bindingTarget.getId() != null) {
+ final String fieldName = LayoutBinderWriterKt.
+ getReadableName(bindingTarget);
+ if (names.contains(fieldName)) {
+ L.w("View field %s collides with a variable or import", fieldName);
+ } else {
+ names.add(fieldName);
+ mExprModel.viewFieldExpr(bindingTarget);
+ }
+ }
+ } finally {
+ Scope.exit();
+ }
+ }
+
+ for (BindingTarget bindingTarget : mBindingTargets) {
+ try {
+ Scope.enter(bindingTarget.mBundle);
+ for (BindingTargetBundle.BindingBundle bindingBundle : bindingTarget.mBundle
.getBindingBundleList()) {
try {
Scope.enter(bindingBundle.getValueLocation());
bindingTarget.addBinding(bindingBundle.getName(),
- parse(bindingBundle.getExpr(), bindingBundle.getValueLocation()));
+ parse(bindingBundle.getExpr(), bindingBundle.isTwoWay(),
+ bindingBundle.getValueLocation()));
} finally {
Scope.exit();
}
}
+ bindingTarget.resolveTwoWayExpressions();
bindingTarget.resolveMultiSetters();
bindingTarget.resolveListeners();
} finally {
@@ -267,9 +288,10 @@ public class LayoutBinder implements FileScopeProvider {
return target;
}
- public Expr parse(String input, @Nullable Location locationInFile) {
+ public Expr parse(String input, boolean isTwoWay, @Nullable Location locationInFile) {
final Expr parsed = mExpressionParser.parse(input, locationInFile);
parsed.setBindingExpression(true);
+ parsed.setTwoWay(isTwoWay);
return parsed;
}
diff --git a/compiler/src/main/java/android/databinding/tool/MergedBinding.java b/compiler/src/main/java/android/databinding/tool/MergedBinding.java
index 07a0fa67..0f64695f 100644
--- a/compiler/src/main/java/android/databinding/tool/MergedBinding.java
+++ b/compiler/src/main/java/android/databinding/tool/MergedBinding.java
@@ -75,6 +75,10 @@ public class MergedBinding extends Binding {
return args.getChildren().toArray(new Expr[args.getChildren().size()]);
}
+ public String[] getAttributes() {
+ return mMultiAttributeSetter.attributes;
+ }
+
@Override
public String getBindingAdapterInstanceClass() {
return mMultiAttributeSetter.getBindingAdapterInstanceClass();
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java
index f16f90b3..c8f6e2cf 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java
@@ -42,7 +42,7 @@ public class ArgListExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
throw new IllegalStateException("should never try to convert an argument expressions"
+ " into code");
}
@@ -61,4 +61,9 @@ public class ArgListExpr extends Expr {
public boolean canBeEvaluatedToAVariable() {
return false;
}
+
+ @Override
+ public String getInvertibleError() {
+ return "Merged bindings are not invertible.";
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/BitShiftExpr.java b/compiler/src/main/java/android/databinding/tool/expr/BitShiftExpr.java
index a5e1ff8c..cbc895bb 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/BitShiftExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/BitShiftExpr.java
@@ -57,10 +57,15 @@ public class BitShiftExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
return new KCode()
- .app("", getLeft().toCode())
+ .app("", getLeft().toCode(expand))
.app(getOp())
- .app("", getRight().toCode());
+ .app("", getRight().toCode(expand));
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return "Bit shift operators cannot be inverted in two-way binding";
}
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/BracketExpr.java b/compiler/src/main/java/android/databinding/tool/expr/BracketExpr.java
index 243d8e99..16c6ce10 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/BracketExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/BracketExpr.java
@@ -65,7 +65,13 @@ public class BracketExpr extends Expr {
}
protected String computeUniqueKey() {
- return join(getTarget().computeUniqueKey(), "$", getArg().computeUniqueKey(), "$");
+ final String targetKey = getTarget().computeUniqueKey();
+ return addTwoWay(join(targetKey, "$", getArg().computeUniqueKey(), "$"));
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return null;
}
public Expr getTarget() {
@@ -81,11 +87,11 @@ public class BracketExpr extends Expr {
}
public boolean argCastsInteger() {
- return getArg().getResolvedType().isObject();
+ return mAccessor != BracketAccessor.MAP && getArg().getResolvedType().isObject();
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
String cast = argCastsInteger() ? "(Integer) " : "";
switch (getAccessor()) {
case ARRAY: {
@@ -120,4 +126,14 @@ public class BracketExpr extends Expr {
}
throw new IllegalStateException("Invalid BracketAccessor type");
}
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ String cast = argCastsInteger() ? "(Integer) " : "";
+ return new KCode().
+ app("setTo(", getTarget().toCode(true)).
+ app(", ").
+ app(cast, getArg().toCode(true)).
+ app(", ", value).app(");");
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/BuiltInVariableExpr.java b/compiler/src/main/java/android/databinding/tool/expr/BuiltInVariableExpr.java
index ab1ab21c..d2fdea13 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/BuiltInVariableExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/BuiltInVariableExpr.java
@@ -16,6 +16,8 @@
package android.databinding.tool.expr;
+import android.databinding.tool.reflection.ModelAnalyzer;
+import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.writer.KCode;
import java.util.ArrayList;
@@ -36,12 +38,18 @@ public class BuiltInVariableExpr extends IdentifierExpr {
}
@Override
+ protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
+ ModelClass modelClass = super.resolveType(modelAnalyzer);
+ return modelClass;
+ }
+
+ @Override
protected List<Dependency> constructDependencies() {
return new ArrayList<Dependency>();
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
if (mAccessCode == null) {
return new KCode().app(mName);
} else {
@@ -52,4 +60,9 @@ public class BuiltInVariableExpr extends IdentifierExpr {
public boolean isDeclared() {
return false;
}
+
+ @Override
+ public String getInvertibleError() {
+ return "Built-in variables may not be the target of two-way binding";
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/CastExpr.java b/compiler/src/main/java/android/databinding/tool/expr/CastExpr.java
index cf8a0ec6..9a4a36ae 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/CastExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/CastExpr.java
@@ -46,7 +46,7 @@ public class CastExpr extends Expr {
}
protected String computeUniqueKey() {
- return join(mType, getCastExpr().computeUniqueKey());
+ return addTwoWay(join(mType, getCastExpr().computeUniqueKey()));
}
public Expr getCastExpr() {
@@ -58,10 +58,21 @@ public class CastExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
return new KCode()
.app("(")
.app(getCastType())
- .app(") ", getCastExpr().toCode());
+ .app(") ", getCastExpr().toCode(expand));
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return getCastExpr().getInvertibleError();
+ }
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ // assume no need to cast in reverse
+ return getCastExpr().toInverseCode(value);
}
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ComparisonExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ComparisonExpr.java
index a5f67e7a..172ea219 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ComparisonExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ComparisonExpr.java
@@ -62,9 +62,14 @@ public class ComparisonExpr extends Expr {
}
@Override
- protected KCode generateCode() {
- return new KCode().app("", getLeft().toCode())
+ protected KCode generateCode(boolean expand) {
+ return new KCode().app("", getLeft().toCode(expand))
.app(" ").app(getOp()).app(" ")
- .app("", getRight().toCode());
+ .app("", getRight().toCode(expand));
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return "Comparison operators are not valid as targets of two-way binding";
}
}
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 bf43fd2a..193501c2 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/Expr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/Expr.java
@@ -98,6 +98,7 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
*/
private boolean mRead;
private boolean mIsUsed = false;
+ private boolean mIsTwoWay = false;
Expr(Iterable<Expr> children) {
for (Expr expr : children) {
@@ -178,6 +179,14 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
return this;
}
+ public Expr resolveTwoWayExpressions(Expr parent) {
+ for (int i = mChildren.size() - 1; i >= 0; i--) {
+ final Expr child = mChildren.get(i);
+ child.resolveTwoWayExpressions(this);
+ }
+ return this;
+ }
+
protected void resetResolvedType() {
mResolvedType = null;
}
@@ -201,6 +210,22 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
mModel = model;
}
+ public void setTwoWay(boolean isTwoWay) {
+ mIsTwoWay = isTwoWay;
+ }
+
+ public boolean isTwoWay() {
+ return mIsTwoWay;
+ }
+
+ protected String addTwoWay(String uniqueKey) {
+ if (mIsTwoWay) {
+ return "twoWay(" + uniqueKey + ")";
+ } else {
+ return "oneWay(" + uniqueKey + ")";
+ }
+ }
+
private BitSet resolveShouldReadWithConditionals() {
// ensure we have invalid flags
BitSet bitSet = new BitSet();
@@ -661,17 +686,38 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider {
}
public KCode toCode() {
- if (isDynamic()) {
+ return toCode(false);
+ }
+
+ protected KCode toCode(boolean expand) {
+ if (!expand && isDynamic()) {
return new KCode(LayoutBinderWriterKt.getExecutePendingLocalName(this));
}
- return generateCode();
+ return generateCode(expand);
}
public KCode toFullCode() {
- return generateCode();
+ return generateCode(false);
+ }
+
+ protected abstract KCode generateCode(boolean expand);
+
+ public KCode toInverseCode(KCode value) {
+ throw new IllegalStateException("expression does not support two-way binding");
}
- protected abstract KCode generateCode();
+ public void assertIsInvertible() {
+ final String errorMessage = getInvertibleError();
+ if (errorMessage != null) {
+ L.e(ErrorMessages.EXPRESSION_NOT_INVERTIBLE, toFullCode().generate(),
+ errorMessage);
+ }
+ }
+
+ /**
+ * @return The reason the expression wasn't invertible or null if it was invertible.
+ */
+ protected abstract String getInvertibleError();
/**
* This expression is the predicate for 1 or more ternary expressions.
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java b/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java
index 8f3b6bf6..058427e0 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java
@@ -16,8 +16,8 @@
package android.databinding.tool.expr;
-import org.antlr.v4.runtime.ParserRuleContext;
-
+import android.databinding.tool.BindingTarget;
+import android.databinding.tool.InverseBinding;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelMethod;
@@ -26,6 +26,8 @@ import android.databinding.tool.util.L;
import android.databinding.tool.util.Preconditions;
import android.databinding.tool.writer.FlagSet;
+import org.antlr.v4.runtime.ParserRuleContext;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
@@ -118,10 +120,6 @@ public class ExprModel {
mCurrentParserContext = currentParserContext;
}
- public void unregister(Expr expr) {
- mExprMap.remove(expr.getUniqueKey());
- }
-
public Map<String, Expr> getExprMap() {
return mExprMap;
}
@@ -166,6 +164,10 @@ public class ExprModel {
return register(new BuiltInVariableExpr(name, type, accessCode));
}
+ public ViewFieldExpr viewFieldExpr(BindingTarget bindingTarget) {
+ return register(new ViewFieldExpr(bindingTarget));
+ }
+
/**
* Creates a static identifier for the given class or returns the existing one.
*/
@@ -246,6 +248,9 @@ public class ExprModel {
return register(new CastExpr(type, expr));
}
+ public TwoWayListenerExpr twoWayListenerExpr(InverseBinding inverseBinding) {
+ return register(new TwoWayListenerExpr(inverseBinding));
+ }
public List<Expr> getBindingExpressions() {
return mBindingExpressions;
}
@@ -282,6 +287,7 @@ public class ExprModel {
}
public void removeExpr(Expr expr) {
+ Preconditions.check(!mSealed, "Can't modify the expression list after sealing the model.");
mBindingExpressions.remove(expr);
mExprMap.remove(expr.computeUniqueKey());
}
@@ -345,6 +351,17 @@ public class ExprModel {
}
}
+ // now all 2-way bound view fields
+ for (Expr expr : mExprMap.values()) {
+ if (expr instanceof FieldAccessExpr) {
+ FieldAccessExpr fieldAccessExpr = (FieldAccessExpr) expr;
+ if (fieldAccessExpr.getChild() instanceof ViewFieldExpr) {
+ flagMapping.add(fieldAccessExpr.getUniqueKey());
+ fieldAccessExpr.setId(counter++);
+ }
+ }
+ }
+
// non-dynamic binding expressions receive some ids so that they can be invalidated
L.d("list of binding expressions");
for (int i = 0; i < mBindingExpressions.size(); i++) {
diff --git a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java
index 516dfe72..ff55a003 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java
@@ -17,6 +17,9 @@
package android.databinding.tool.expr;
import android.databinding.tool.ext.ExtKt;
+import android.databinding.tool.Binding;
+import android.databinding.tool.BindingTarget;
+import android.databinding.tool.InverseBinding;
import android.databinding.tool.processing.Scope;
import android.databinding.tool.reflection.Callable;
import android.databinding.tool.reflection.Callable.Type;
@@ -24,6 +27,8 @@ import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelMethod;
import android.databinding.tool.util.BrNameUtil;
+import android.databinding.tool.store.SetterStore;
+import android.databinding.tool.store.SetterStore.BindingGetterCall;
import android.databinding.tool.util.L;
import android.databinding.tool.util.Preconditions;
import android.databinding.tool.writer.KCode;
@@ -37,6 +42,7 @@ public class FieldAccessExpr extends Expr {
Callable mGetter;
final boolean mIsObservableField;
boolean mIsListener;
+ boolean mIsViewAttributeAccess;
FieldAccessExpr(Expr parent, String name) {
super(parent);
@@ -61,6 +67,19 @@ public class FieldAccessExpr extends Expr {
return mGetter;
}
+ @Override
+ public String getInvertibleError() {
+ if (getGetter().setterName == null) {
+ return "Two-way binding cannot resolve a setter for " + getResolvedType().toJavaCode() +
+ " property '" + mName + "'";
+ }
+ if (!mGetter.isDynamic()) {
+ return "Cannot change a final field in " + getResolvedType().toJavaCode() +
+ " property " + mName;
+ }
+ return null;
+ }
+
public int getMinApi() {
return mGetter.getMinApi();
}
@@ -79,6 +98,10 @@ public class FieldAccessExpr extends Expr {
return !mGetter.isStatic() || mGetter.isDynamic();
}
+ if (mIsViewAttributeAccess) {
+ return true; // must be able to invalidate this
+ }
+
// if owner is NOT dynamic, we can be dynamic if an only if getter is dynamic
return mGetter.isDynamic();
}
@@ -182,9 +205,9 @@ public class FieldAccessExpr extends Expr {
@Override
protected String computeUniqueKey() {
if (mIsObservableField) {
- return join(mName, "..", super.computeUniqueKey());
+ return addTwoWay(join(mName, "..", super.computeUniqueKey()));
}
- return join(mName, ".", super.computeUniqueKey());
+ return addTwoWay(join(mName, ".", super.computeUniqueKey()));
}
public String getName() {
@@ -257,7 +280,7 @@ public class FieldAccessExpr extends Expr {
getChildren().add(observableField);
observableField.getParents().add(this);
- mGetter = mGetter.resolvedType.findGetterOrField("get", false);
+ mGetter = mGetter.resolvedType.findGetterOrField("", false);
mName = "";
mBrName = ExtKt.br(mName);
} else if (hasBindableAnnotations()) {
@@ -268,18 +291,123 @@ public class FieldAccessExpr extends Expr {
}
@Override
+ public Expr resolveTwoWayExpressions(Expr parent) {
+ final Expr child = getChild();
+ if (!(child instanceof ViewFieldExpr)) {
+ return this;
+ }
+ final ViewFieldExpr expr = (ViewFieldExpr) child;
+ final BindingTarget bindingTarget = expr.getBindingTarget();
+
+ // This is a binding to a View's attribute, so look for matching attribute
+ // on that View's BindingTarget. If there is an expression, we simply replace
+ // the binding with that binding expression.
+ for (Binding binding : bindingTarget.getBindings()) {
+ if (attributeMatchesName(binding.getName(), mName)) {
+ final Expr replacement = binding.getExpr();
+ replaceExpression(parent, replacement);
+ return replacement;
+ }
+ }
+
+ // There was no binding expression to bind to. This should be a two-way binding.
+ // This is a synthesized two-way binding because we must capture the events from
+ // the View and change the value when the target View's attribute changes.
+ final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance());
+ final ModelClass targetClass = expr.getResolvedType();
+ BindingGetterCall getter = setterStore.getGetterCall(mName, targetClass, null, null);
+ if (getter == null) {
+ getter = setterStore.getGetterCall("android:" + mName, targetClass, null, null);
+ if (getter == null) {
+ L.e("Could not resolve the two-way binding attribute '%s' on type '%s'",
+ mName, targetClass);
+ }
+ }
+ InverseBinding inverseBinding = null;
+ for (Binding binding : bindingTarget.getBindings()) {
+ final Expr testExpr = binding.getExpr();
+ if (testExpr instanceof TwoWayListenerExpr &&
+ getter.getEventAttribute().equals(binding.getName())) {
+ inverseBinding = ((TwoWayListenerExpr) testExpr).mInverseBinding;
+ break;
+ }
+ }
+ if (inverseBinding == null) {
+ inverseBinding = bindingTarget.addInverseBinding(mName, getter);
+ }
+ inverseBinding.addChainedExpression(this);
+ mIsViewAttributeAccess = true;
+ enableDirectInvalidation();
+ return this;
+ }
+
+ private static boolean attributeMatchesName(String attribute, String field) {
+ int colonIndex = attribute.indexOf(':');
+ return attribute.substring(colonIndex + 1).equals(field);
+ }
+
+ private void replaceExpression(Expr parent, Expr replacement) {
+ if (parent != null) {
+ List<Expr> children = parent.getChildren();
+ int index;
+ while ((index = children.indexOf(this)) >= 0) {
+ children.set(index, replacement);
+ replacement.getParents().add(parent);
+ }
+ while (getParents().remove(parent)) {
+ // just remove all copies of parent.
+ }
+ }
+ if (getParents().isEmpty()) {
+ getModel().removeExpr(this);
+ }
+ }
+
+ @Override
protected String asPackage() {
String parentPackage = getChild().asPackage();
return parentPackage == null ? null : parentPackage + "." + mName;
}
@Override
- protected KCode generateCode() {
- KCode code = new KCode().app("", getChild().toCode()).app(".");
+ protected KCode generateCode(boolean expand) {
+ KCode code = new KCode();
+ if (expand) {
+ String defaultValue = ModelAnalyzer.getInstance().getDefaultValue(
+ getResolvedType().toJavaCode());
+ code.app("(", getChild().toCode(true))
+ .app(" == null) ? ")
+ .app(defaultValue)
+ .app(" : ");
+ }
+ code.app("", getChild().toCode(expand)).app(".");
if (getGetter().type == Callable.Type.FIELD) {
return code.app(getGetter().name);
} else {
return code.app(getGetter().name).app("()");
}
}
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ if (mGetter.setterName == null) {
+ throw new IllegalStateException("There is no inverse for " + toCode().generate());
+ }
+ KCode castValue = new KCode("(").app(getResolvedType().toJavaCode() + ")(", value).app(")");
+ String type = getChild().getResolvedType().toJavaCode();
+ KCode code = new KCode("targetObj_.");
+ if (getGetter().type == Callable.Type.FIELD) {
+ code.app(getGetter().setterName).app(" = ", castValue).app(";");
+ } else {
+ code.app(getGetter().setterName).app("(", castValue).app(")").app(";");
+ }
+ return new KCode()
+ .app("final ")
+ .app(type)
+ .app(" targetObj_ = ", getChild().toCode(true))
+ .app(";")
+ .nl(new KCode("if (targetObj_ != null) {"))
+ .tab(code)
+ .nl(new KCode("}"));
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/GroupExpr.java b/compiler/src/main/java/android/databinding/tool/expr/GroupExpr.java
index 813c03cc..4a76688b 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/GroupExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/GroupExpr.java
@@ -38,11 +38,22 @@ public class GroupExpr extends Expr {
}
@Override
- protected KCode generateCode() {
- return new KCode().app("(", getWrapped().toCode()).app(")");
+ protected KCode generateCode(boolean expand) {
+ return new KCode().app("(", getWrapped().toCode(expand)).app(")");
}
public Expr getWrapped() {
return getChildren().get(0);
}
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ // Nothing to do here. Other expressions should automatically take care of grouping.
+ return getWrapped().toInverseCode(value);
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return getWrapped().getInvertibleError();
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/IdentifierExpr.java b/compiler/src/main/java/android/databinding/tool/expr/IdentifierExpr.java
index 3b1b2f9c..efe4fb15 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/IdentifierExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/IdentifierExpr.java
@@ -78,8 +78,12 @@ public class IdentifierExpr extends Expr {
}
@Override
- protected KCode generateCode() {
- return new KCode(LayoutBinderWriterKt.getExecutePendingLocalName(this));
+ protected KCode generateCode(boolean expand) {
+ if (expand) {
+ return new KCode(LayoutBinderWriterKt.getFieldName(this));
+ } else {
+ return new KCode(LayoutBinderWriterKt.getExecutePendingLocalName(this));
+ }
}
public void setDeclared() {
@@ -89,4 +93,14 @@ public class IdentifierExpr extends Expr {
public boolean isDeclared() {
return mIsDeclared;
}
+
+ @Override
+ public String getInvertibleError() {
+ return null;
+ }
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ return new KCode().app(LayoutBinderWriterKt.getSetterName(this)).app("(", value).app(");");
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/InstanceOfExpr.java b/compiler/src/main/java/android/databinding/tool/expr/InstanceOfExpr.java
index 449a1958..980d6356 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/InstanceOfExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/InstanceOfExpr.java
@@ -37,9 +37,9 @@ public class InstanceOfExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
return new KCode()
- .app("", getExpr().toCode())
+ .app("", getExpr().toCode(expand))
.app(" instanceof ")
.app(getType().toJavaCode());
}
@@ -62,4 +62,9 @@ public class InstanceOfExpr extends Expr {
public ModelClass getType() {
return mType;
}
+
+ @Override
+ public String getInvertibleError() {
+ return "two-way binding can't target a value with the 'instanceof' operator";
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ListenerExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ListenerExpr.java
index 6b99376e..6adf997c 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ListenerExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ListenerExpr.java
@@ -81,7 +81,7 @@ public class ListenerExpr extends Expr {
}
@Override
- public KCode generateCode() {
+ public KCode generateCode(boolean expand) {
KCode code = new KCode("(");
final int minApi = Math.max(mListenerType.getMinApi(), mMethod.getMinApi());
if (minApi > 1) {
@@ -106,4 +106,9 @@ public class ListenerExpr extends Expr {
code.app(")");
return code;
}
+
+ @Override
+ public String getInvertibleError() {
+ return "Listeners cannot be the target of a two-way binding";
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/MathExpr.java b/compiler/src/main/java/android/databinding/tool/expr/MathExpr.java
index cc16eb94..a302659d 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/MathExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/MathExpr.java
@@ -31,7 +31,7 @@ public class MathExpr extends Expr {
@Override
protected String computeUniqueKey() {
- return join(getLeft().getUniqueKey(), mOp, getRight().getUniqueKey());
+ return addTwoWay(join(getLeft().getUniqueKey(), mOp, getRight().getUniqueKey()));
}
@Override
@@ -65,7 +65,81 @@ public class MathExpr extends Expr {
}
@Override
- protected KCode generateCode() {
- return new KCode().app("", getLeft().toCode()).app(mOp, getRight().toCode());
+ protected KCode generateCode(boolean expand) {
+ return new KCode().app("", getLeft().toCode(expand)).app(mOp, getRight().toCode(expand));
+ }
+
+ @Override
+ public String getInvertibleError() {
+ if (mOp.equals("%")) {
+ return "The modulus operator (%) is not supported in two-way binding.";
+ } else if (getResolvedType().isString()) {
+ return "String concatenation operator (+) is not supported in two-way binding.";
+ }
+ if (!getLeft().isDynamic()) {
+ return getRight().getInvertibleError();
+ } else if (!getRight().isDynamic()) {
+ return getLeft().getInvertibleError();
+ } else {
+ return "Arithmetic operator " + mOp + " is not supported with two dynamic expressions.";
+ }
+ }
+
+ private String inverseCast() {
+ if (!getLeft().isDynamic()) {
+ return inverseCast(getRight());
+ } else {
+ return inverseCast(getLeft());
+ }
+ }
+
+ private String inverseCast(Expr expr) {
+ if (!expr.getResolvedType().isAssignableFrom(getResolvedType())) {
+ return "(" + getResolvedType() + ")";
+ }
+ return null;
+ }
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ if (!isDynamic()) {
+ return toCode();
+ }
+ final Expr left = getLeft();
+ final Expr right = getRight();
+ final Expr constExpr = left.isDynamic() ? right : left;
+ final Expr varExpr = left.isDynamic() ? left : right;
+ final String cast = inverseCast();
+ if (cast != null) {
+ value = new KCode(cast).app("(", value).app(")");
+ }
+ switch (mOp.charAt(0)) {
+ case '+': // const + x = value => x = value - const
+ return varExpr.toInverseCode(value.app(" - (", constExpr.toCode()).app(")"));
+ case '*': // const * x = value => x = value / const
+ return varExpr.toInverseCode(value.app(" / (", constExpr.toCode()).app(")"));
+ case '-':
+ if (!left.isDynamic()) { // const - x = value => x = const - value)
+ return varExpr.toInverseCode(new KCode()
+ .app("(", constExpr.toCode())
+ .app(") - (", value)
+ .app(")"));
+ } else { // x - const = value => x = value + const)
+ return varExpr.toInverseCode(value.app(" + ", constExpr.toCode()));
+ }
+ case '/':
+ if (!left.isDynamic()) { // const / x = value => x = const / value
+ return varExpr.toInverseCode(new KCode("(")
+ .app("", constExpr.toCode())
+ .app(") / (", value)
+ .app(")"));
+ } else { // x / const = value => x = value * const
+ return varExpr.toInverseCode(new KCode("(")
+ .app("", value)
+ .app(") * (", constExpr.toCode())
+ .app(")"));
+ }
+ }
+ throw new IllegalStateException("Invalid math operation is not invertible: " + mOp);
}
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java b/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java
index a81cffe6..4990981d 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java
@@ -16,6 +16,9 @@
package android.databinding.tool.expr;
+import static android.databinding.tool.reflection.Callable.DYNAMIC;
+import static android.databinding.tool.reflection.Callable.STATIC;
+
import android.databinding.tool.processing.Scope;
import android.databinding.tool.reflection.Callable;
import android.databinding.tool.reflection.Callable.Type;
@@ -28,9 +31,6 @@ import android.databinding.tool.writer.KCode;
import java.util.ArrayList;
import java.util.List;
-import static android.databinding.tool.reflection.Callable.DYNAMIC;
-import static android.databinding.tool.reflection.Callable.STATIC;
-
public class MethodCallExpr extends Expr {
final String mName;
@@ -61,9 +61,9 @@ public class MethodCallExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
KCode code = new KCode()
- .app("", getTarget().toCode())
+ .app("", getTarget().toCode(expand))
.app(".")
.app(getGetter().name)
.app("(");
@@ -74,7 +74,7 @@ public class MethodCallExpr extends Expr {
} else {
code.app(", ");
}
- code.app("", arg.toCode());
+ code.app("", arg.toCode(expand));
}
code.app(")");
return code;
@@ -114,7 +114,7 @@ public class MethodCallExpr extends Expr {
if (method.isStatic()) {
flags |= STATIC;
}
- mGetter = new Callable(Type.METHOD, method.getName(), method.getReturnType(args),
+ mGetter = new Callable(Type.METHOD, method.getName(), null, method.getReturnType(args),
method.getParameterTypes().length, flags);
}
return mGetter.resolvedType;
@@ -152,4 +152,9 @@ public class MethodCallExpr extends Expr {
public Callable getGetter() {
return mGetter;
}
+
+ @Override
+ public String getInvertibleError() {
+ return "Method calls may not be used in two-way expressions";
+ }
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
index f1cab64c..7a3b853f 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
@@ -130,7 +130,7 @@ public class ResourceExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
return new KCode(toJava());
}
@@ -138,6 +138,12 @@ public class ResourceExpr extends Expr {
return mPackage + "R." + getResourceObject() + "." + mResourceId;
}
+ @Override
+ public String getInvertibleError() {
+ return "Resources may not be the target of a two-way binding expression: " +
+ computeUniqueKey();
+ }
+
public String toJava() {
final String context = "getRoot().getContext()";
final String resources = "getRoot().getResources()";
diff --git a/compiler/src/main/java/android/databinding/tool/expr/StaticIdentifierExpr.java b/compiler/src/main/java/android/databinding/tool/expr/StaticIdentifierExpr.java
index d64584ae..7618e946 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/StaticIdentifierExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/StaticIdentifierExpr.java
@@ -35,7 +35,17 @@ public class StaticIdentifierExpr extends IdentifierExpr {
}
@Override
- protected KCode generateCode() {
+ public String getInvertibleError() {
+ return "Class " + getResolvedType().toJavaCode() +
+ " may not be the target of a two-way binding expression";
+ }
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ throw new IllegalStateException("StaticIdentifierExpr is not invertible.");
+ }
+ @Override
+ protected KCode generateCode(boolean expand) {
return new KCode(getResolvedType().toJavaCode());
}
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/SymbolExpr.java b/compiler/src/main/java/android/databinding/tool/expr/SymbolExpr.java
index 6af6c298..38708c04 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/SymbolExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/SymbolExpr.java
@@ -48,7 +48,12 @@ public class SymbolExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ public String getInvertibleError() {
+ return "Symbol '" + mText + "' cannot be the target of a two-way binding expression";
+ }
+
+ @Override
+ protected KCode generateCode(boolean expand) {
return new KCode(getText());
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/TernaryExpr.java b/compiler/src/main/java/android/databinding/tool/expr/TernaryExpr.java
index 90076c4c..d4a37274 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/TernaryExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/TernaryExpr.java
@@ -47,6 +47,20 @@ public class TernaryExpr extends Expr {
}
@Override
+ public String getInvertibleError() {
+ if (getPred().isDynamic()) {
+ return "The condition of a ternary operator must be constant: " +
+ getPred().toFullCode();
+ }
+ final String trueInvertible = getIfTrue().getInvertibleError();
+ if (trueInvertible != null) {
+ return trueInvertible;
+ } else {
+ return getIfFalse().getInvertibleError();
+ }
+ }
+
+ @Override
protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
final Expr ifTrue = getIfTrue();
final Expr ifFalse = getIfFalse();
@@ -90,15 +104,26 @@ public class TernaryExpr extends Expr {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean expand) {
return new KCode()
- .app("", getPred().toCode())
- .app(" ? ", getIfTrue().toCode())
- .app(" : ", getIfFalse().toCode());
+ .app("", getPred().toCode(expand))
+ .app(" ? ", getIfTrue().toCode(expand))
+ .app(" : ", getIfFalse().toCode(expand));
}
@Override
+ public KCode toInverseCode(KCode variable) {
+ return new KCode()
+ .app("if (", getPred().toCode(true))
+ .app(") {")
+ .tab(getIfTrue().toInverseCode(variable))
+ .nl(new KCode("} else {"))
+ .tab(getIfFalse().toInverseCode(variable))
+ .nl(new KCode("}"));
+ }
+
+ @Override
public boolean isConditional() {
return true;
}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/TwoWayListenerExpr.java b/compiler/src/main/java/android/databinding/tool/expr/TwoWayListenerExpr.java
new file mode 100644
index 00000000..1a656732
--- /dev/null
+++ b/compiler/src/main/java/android/databinding/tool/expr/TwoWayListenerExpr.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.expr;
+
+import android.databinding.InverseBindingListener;
+import android.databinding.tool.InverseBinding;
+import android.databinding.tool.reflection.ModelAnalyzer;
+import android.databinding.tool.reflection.ModelClass;
+import android.databinding.tool.writer.KCode;
+import android.databinding.tool.writer.LayoutBinderWriterKt;
+
+import java.util.List;
+
+/**
+ * TwoWayListenerExpr is used to set the event listener for a two-way binding expression.
+ */
+public class TwoWayListenerExpr extends Expr {
+ final InverseBinding mInverseBinding;
+
+ public TwoWayListenerExpr(InverseBinding inverseBinding) {
+ mInverseBinding = inverseBinding;
+ }
+
+ @Override
+ protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
+ return modelAnalyzer.findClass(InverseBindingListener.class);
+ }
+
+ @Override
+ protected List<Dependency> constructDependencies() {
+ return constructDynamicChildrenDependencies();
+ }
+
+ @Override
+ protected KCode generateCode(boolean expand) {
+ final String fieldName = LayoutBinderWriterKt.getFieldName(mInverseBinding);
+ return new KCode(fieldName);
+ }
+
+ @Override
+ protected String computeUniqueKey() {
+ return "event(" + mInverseBinding.getEventAttribute() + ", " +
+ System.identityHashCode(mInverseBinding) + ")";
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return "Inverted expressions are already inverted!";
+ }
+}
diff --git a/compiler/src/main/java/android/databinding/tool/expr/UnaryExpr.java b/compiler/src/main/java/android/databinding/tool/expr/UnaryExpr.java
index c911d460..881a352c 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/UnaryExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/UnaryExpr.java
@@ -30,13 +30,23 @@ public class UnaryExpr extends Expr {
}
@Override
+ public String getInvertibleError() {
+ return getExpr().getInvertibleError();
+ }
+
+ @Override
protected String computeUniqueKey() {
- return join(getOpStr(), getExpr().getUniqueKey());
+ return addTwoWay(join(getOpStr(), getExpr().getUniqueKey()));
+ }
+
+ @Override
+ public KCode toInverseCode(KCode value) {
+ return getExpr().toInverseCode(new KCode().app(mOp, value));
}
@Override
- protected KCode generateCode() {
- return new KCode().app(getOp(), getExpr().toCode());
+ protected KCode generateCode(boolean expand) {
+ return new KCode().app(getOp(), getExpr().toCode(expand));
}
@Override
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ViewFieldExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ViewFieldExpr.java
new file mode 100644
index 00000000..0a6b15b1
--- /dev/null
+++ b/compiler/src/main/java/android/databinding/tool/expr/ViewFieldExpr.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.expr;
+
+import android.databinding.tool.BindingTarget;
+import android.databinding.tool.reflection.ModelAnalyzer;
+import android.databinding.tool.reflection.ModelClass;
+import android.databinding.tool.writer.LayoutBinderWriterKt;
+
+public class ViewFieldExpr extends BuiltInVariableExpr {
+ private final BindingTarget mBindingTarget;
+
+ ViewFieldExpr(BindingTarget bindingTarget) {
+ super(LayoutBinderWriterKt.getFieldName(bindingTarget), initialType(bindingTarget),
+ LayoutBinderWriterKt.getFieldName(bindingTarget));
+ mBindingTarget = bindingTarget;
+ }
+
+ @Override
+ public String getInvertibleError() {
+ return "View fields may not be the target of two-way binding";
+ }
+
+ private static String initialType(BindingTarget bindingTarget) {
+ return bindingTarget.isBinder()
+ ? "android.databinding.ViewDataBinding"
+ : bindingTarget.getInterfaceType();
+ }
+
+ public BindingTarget getBindingTarget() {
+ return mBindingTarget;
+ }
+
+ @Override
+ protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
+ final ModelClass type = modelAnalyzer.findClass(mBindingTarget.getInterfaceType(), null);
+ if (type == null) {
+ return modelAnalyzer.findClass("android.databinding.ViewDataBinding", null);
+ }
+ return type;
+ }
+}
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/Callable.java b/compiler/src/main/java/android/databinding/tool/reflection/Callable.java
index bae7f7cf..5b9acf27 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/Callable.java
+++ b/compiler/src/main/java/android/databinding/tool/reflection/Callable.java
@@ -15,10 +15,6 @@
*/
package android.databinding.tool.reflection;
-import android.databinding.tool.util.L;
-
-import java.util.List;
-
public class Callable {
public enum Type {
@@ -34,17 +30,21 @@ public class Callable {
public final String name;
+ public final String setterName;
+
public final ModelClass resolvedType;
private final int mFlags;
private final int mParameterCount;
- public Callable(Type type, String name, ModelClass resolvedType, int parameterCount, int flags) {
+ public Callable(Type type, String name, String setterName, ModelClass resolvedType,
+ int parameterCount, int flags) {
this.type = type;
this.name = name;
this.resolvedType = resolvedType;
mParameterCount = parameterCount;
+ this.setterName = setterName;
mFlags = flags;
}
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java
index 1dd14892..6fbc5a0f 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java
+++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java
@@ -384,8 +384,8 @@ public abstract class ModelClass {
*/
public Callable findGetterOrField(String name, boolean staticOnly) {
if ("length".equals(name) && isArray()) {
- return new Callable(Type.FIELD, name, ModelAnalyzer.getInstance().loadPrimitive("int"),
- 0, 0);
+ return new Callable(Type.FIELD, name, null,
+ ModelAnalyzer.getInstance().loadPrimitive("int"), 0, 0);
}
String capitalized = StringUtils.capitalize(name);
String[] methodNames = {
@@ -413,8 +413,10 @@ public abstract class ModelClass {
flags |= CAN_BE_INVALIDATED;
}
}
+ final ModelMethod setterMethod = findSetter(method, name);
+ final String setterName = setterMethod == null ? null : setterMethod.getName();
final Callable result = new Callable(Callable.Type.METHOD, methodName,
- method.getReturnType(null), method.getParameterTypes().length,
+ setterName, method.getReturnType(null), method.getParameterTypes().length,
flags);
return result;
}
@@ -438,16 +440,37 @@ public abstract class ModelClass {
}
ModelClass fieldType = publicField.getFieldType();
int flags = 0;
+ String setterFieldName = name;
+ if (publicField.isStatic()) {
+ flags |= STATIC;
+ }
if (!publicField.isFinal()) {
+ setterFieldName = null;
flags |= DYNAMIC;
}
if (publicField.isBindable()) {
flags |= CAN_BE_INVALIDATED;
}
- if (publicField.isStatic()) {
- flags |= STATIC;
+ return new Callable(Callable.Type.FIELD, name, setterFieldName, fieldType, 0, flags);
+ }
+
+ public ModelMethod findInstanceGetter(String name) {
+ String capitalized = StringUtils.capitalize(name);
+ String[] methodNames = {
+ "get" + capitalized,
+ "is" + capitalized,
+ name
+ };
+ for (String methodName : methodNames) {
+ ModelMethod[] methods = getMethods(methodName, new ArrayList<ModelClass>(), false);
+ for (ModelMethod method : methods) {
+ if (method.isPublic() && !method.isStatic() &&
+ !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) {
+ return method;
+ }
+ }
}
- return new Callable(Callable.Type.FIELD, name, fieldType, 0, flags);
+ return null;
}
private ModelField getField(String name, boolean allowPrivate, boolean isStatic) {
@@ -463,6 +486,33 @@ public abstract class ModelClass {
return null;
}
+ private ModelMethod findSetter(ModelMethod getter, String originalName) {
+ final String capitalized = StringUtils.capitalize(originalName);
+ final String[] possibleNames;
+ if (originalName.equals(getter.getName())) {
+ possibleNames = new String[] { originalName, "set" + capitalized };
+ } else if (getter.getName().startsWith("is")){
+ possibleNames = new String[] { "set" + capitalized, "setIs" + capitalized };
+ } else {
+ possibleNames = new String[] { "set" + capitalized };
+ }
+ for (String name : possibleNames) {
+ List<ModelMethod> methods = findMethods(name, getter.isStatic());
+ if (methods != null) {
+ ModelClass param = getter.getReturnType(null);
+ for (ModelMethod method : methods) {
+ ModelClass[] parameterTypes = method.getParameterTypes();
+ if (parameterTypes != null && parameterTypes.length == 1 &&
+ parameterTypes[0].equals(param) &&
+ method.isStatic() == getter.isStatic()) {
+ return method;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
/**
* Finds public methods that matches the given name exactly. These may be resolved into
* listener methods during Expr.resolveListeners.
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationAnalyzer.java b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationAnalyzer.java
index 4f110889..4773f17d 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationAnalyzer.java
+++ b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationAnalyzer.java
@@ -24,7 +24,10 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
@@ -55,8 +58,13 @@ public class AnnotationAnalyzer extends ModelAnalyzer {
setInstance(this);
L.setClient(new L.Client() {
@Override
- public void printMessage(Diagnostic.Kind kind, String message) {
- mProcessingEnv.getMessager().printMessage(kind, message);
+ public void printMessage(Diagnostic.Kind kind, String message, Element element) {
+ Messager messager = mProcessingEnv.getMessager();
+ if (element != null) {
+ messager.printMessage(kind, message, element);
+ } else {
+ messager.printMessage(kind, message);
+ }
}
});
}
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java
index 26ca5736..d7caa452 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java
+++ b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java
@@ -23,12 +23,16 @@ import android.databinding.tool.reflection.TypeUtil;
import java.util.List;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
class AnnotationMethod extends ModelMethod {
@@ -36,6 +40,7 @@ class AnnotationMethod extends ModelMethod {
final DeclaredType mDeclaringType;
final ExecutableElement mExecutableElement;
int mApiLevel = -1; // calculated on demand
+ ModelClass mReceiverType;
public AnnotationMethod(DeclaredType declaringType, ExecutableElement executableElement) {
mDeclaringType = declaringType;
@@ -46,7 +51,46 @@ class AnnotationMethod extends ModelMethod {
@Override
public ModelClass getDeclaringClass() {
- return new AnnotationClass(mDeclaringType);
+ if (mReceiverType == null) {
+ mReceiverType = findReceiverType(mDeclaringType);
+ if (mReceiverType == null) {
+ mReceiverType = new AnnotationClass(mDeclaringType);
+ }
+ }
+ return mReceiverType;
+ }
+
+ // TODO: When going to Java 1.8, use mExecutableElement.getReceiverType()
+ private ModelClass findReceiverType(DeclaredType subType) {
+ List<? extends TypeMirror> supers = getTypeUtils().directSupertypes(subType);
+ for (TypeMirror superType : supers) {
+ if (superType.getKind() == TypeKind.DECLARED) {
+ DeclaredType declaredType = (DeclaredType) superType;
+ ModelClass inSuper = findReceiverType(declaredType);
+ if (inSuper != null) {
+ return inSuper;
+ } else if (hasExecutableMethod(declaredType)) {
+ return new AnnotationClass(declaredType);
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean hasExecutableMethod(DeclaredType declaredType) {
+ Elements elementUtils = getElementUtils();
+ TypeElement enclosing = (TypeElement) mExecutableElement.getEnclosingElement();
+ TypeElement typeElement = (TypeElement) declaredType.asElement();
+ for (Element element : typeElement.getEnclosedElements()) {
+ if (element.getKind() == ElementKind.METHOD) {
+ ExecutableElement executableElement = (ExecutableElement) element;
+ if (executableElement.equals(mExecutableElement) ||
+ elementUtils.overrides(mExecutableElement, executableElement, enclosing)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
@Override
@@ -115,6 +159,14 @@ class AnnotationMethod extends ModelMethod {
return mExecutableElement.isVarArgs();
}
+ private static Types getTypeUtils() {
+ return AnnotationAnalyzer.get().mProcessingEnv.getTypeUtils();
+ }
+
+ private static Elements getElementUtils() {
+ return AnnotationAnalyzer.get().mProcessingEnv.getElementUtils();
+ }
+
@Override
public String toString() {
return "AnnotationMethod{" +
diff --git a/compiler/src/main/java/android/databinding/tool/store/SetterStore.java b/compiler/src/main/java/android/databinding/tool/store/SetterStore.java
index ffeb06b6..95688100 100644
--- a/compiler/src/main/java/android/databinding/tool/store/SetterStore.java
+++ b/compiler/src/main/java/android/databinding/tool/store/SetterStore.java
@@ -15,6 +15,7 @@
*/
package android.databinding.tool.store;
+import android.databinding.InverseBindingListener;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelMethod;
@@ -50,9 +51,10 @@ import javax.lang.model.type.TypeMirror;
public class SetterStore {
private static SetterStore sStore;
- private final IntermediateV1 mStore;
+ private final IntermediateV2 mStore;
private final ModelAnalyzer mClassAnalyzer;
private HashMap<String, List<String>> mInstanceAdapters;
+ private final HashSet<String> mInverseEventAttributes = new HashSet<String>();
private Comparator<MultiAttributeSetter> COMPARE_MULTI_ATTRIBUTE_SETTERS =
new Comparator<MultiAttributeSetter>() {
@@ -141,9 +143,19 @@ public class SetterStore {
}
};
- private SetterStore(ModelAnalyzer modelAnalyzer, IntermediateV1 store) {
+ private SetterStore(ModelAnalyzer modelAnalyzer, IntermediateV2 store) {
mClassAnalyzer = modelAnalyzer;
mStore = store;
+ for (HashMap<AccessorKey, InverseDescription> adapter : mStore.inverseAdapters.values()) {
+ for (InverseDescription inverseDescription : adapter.values()) {
+ mInverseEventAttributes.add(inverseDescription.event);
+ }
+ }
+ for (HashMap<String, InverseDescription> method : mStore.inverseMethods.values()) {
+ for (InverseDescription inverseDescription : method.values()) {
+ mInverseEventAttributes.add(inverseDescription.event);
+ }
+ }
}
public static SetterStore get(ModelAnalyzer modelAnalyzer) {
@@ -154,7 +166,7 @@ public class SetterStore {
}
private static SetterStore load(ModelAnalyzer modelAnalyzer) {
- IntermediateV1 store = new IntermediateV1();
+ IntermediateV2 store = new IntermediateV2();
List<Intermediate> previousStores = GenerationalClassUtil
.loadObjects(GenerationalClassUtil.ExtensionFilter.SETTER_STORE);
for (Intermediate intermediate : previousStores) {
@@ -177,6 +189,21 @@ public class SetterStore {
renamed.put(declaringClass, methodDescription);
}
+ public void addInverseMethod(String attribute, String event, String declaringClass,
+ String method, TypeElement declaredOn) {
+ attribute = stripNamespace(attribute);
+ event = stripNamespace(event);
+ HashMap<String, InverseDescription> inverseMethods = mStore.inverseMethods.get(attribute);
+ if (inverseMethods == null) {
+ inverseMethods = new HashMap<String, InverseDescription>();
+ mStore.inverseMethods.put(attribute, inverseMethods);
+ }
+ InverseDescription methodDescription = new InverseDescription(
+ declaredOn.getQualifiedName().toString(), method, event);
+ L.d("STORE addInverseMethod desc %s", methodDescription);
+ inverseMethods.put(declaringClass, methodDescription);
+ }
+
public void addBindingAdapter(ProcessingEnvironment processingEnv, String attribute,
ExecutableElement bindingMethod, boolean takesComponent) {
attribute = stripNamespace(attribute);
@@ -202,6 +229,32 @@ public class SetterStore {
adapters.put(key, new MethodDescription(bindingMethod, 1, takesComponent));
}
+ public void addInverseAdapter(ProcessingEnvironment processingEnv, String attribute,
+ String event, ExecutableElement bindingMethod, boolean takesComponent) {
+ attribute = stripNamespace(attribute);
+ event = stripNamespace(event);
+ L.d("STORE addInverseAdapter %s %s", attribute, bindingMethod);
+ HashMap<AccessorKey, InverseDescription> adapters = mStore.inverseAdapters.get(attribute);
+
+ if (adapters == null) {
+ adapters = new HashMap<AccessorKey, InverseDescription>();
+ mStore.inverseAdapters.put(attribute, adapters);
+ }
+ List<? extends VariableElement> parameters = bindingMethod.getParameters();
+ final int viewIndex = takesComponent ? 1 : 0;
+ TypeMirror viewType = eraseType(processingEnv, parameters.get(viewIndex).asType());
+ String view = getQualifiedName(viewType);
+ TypeMirror returnType = eraseType(processingEnv, bindingMethod.getReturnType());
+ String value = getQualifiedName(returnType);
+
+ AccessorKey key = new AccessorKey(view, value);
+ if (adapters.containsKey(key)) {
+ throw new IllegalArgumentException("Already exists!");
+ }
+
+ adapters.put(key, new InverseDescription(bindingMethod, event, takesComponent));
+ }
+
private static TypeMirror eraseType(ProcessingEnvironment processingEnv,
TypeMirror typeMirror) {
if (hasTypeVar(typeMirror)) {
@@ -274,7 +327,9 @@ public class SetterStore {
private static String[] stripAttributes(String[] attributes) {
String[] strippedAttributes = new String[attributes.length];
for (int i = 0; i < attributes.length; i++) {
- strippedAttributes[i] = stripNamespace(attributes[i]);
+ if (attributes[i] != null) {
+ strippedAttributes[i] = stripNamespace(attributes[i]);
+ }
}
return strippedAttributes;
}
@@ -398,6 +453,10 @@ public class SetterStore {
return attribute;
}
+ public boolean isTwoWayEventAttribute(String attribute) {
+ attribute = stripNamespace(attribute);
+ return mInverseEventAttributes.contains(attribute);
+ }
public List<MultiAttributeSetter> getMultiAttributeSetterCalls(String[] attributes,
ModelClass viewType, ModelClass[] valueType) {
attributes = stripAttributes(attributes);
@@ -461,6 +520,13 @@ public class SetterStore {
adapters.add(method.type);
}
}
+ for (Map<AccessorKey, InverseDescription> methods : mStore.inverseAdapters.values()) {
+ for (InverseDescription method : methods.values()) {
+ if (!method.isStatic) {
+ adapters.add(method.type);
+ }
+ }
+ }
mInstanceAdapters = new HashMap<String, List<String>>();
for (String adapter : adapters) {
final String simpleName = simpleName(adapter);
@@ -609,7 +675,7 @@ public class SetterStore {
adapters.get(key).method, adapterValueType.toJavaCode(),
valueType.toJavaCode());
boolean isBetterView = bestViewType == null ||
- bestValueType.isAssignableFrom(adapterValueType);
+ bestViewType.isAssignableFrom(adapterViewType);
if (isBetterParameter(valueType, adapterValueType, bestValueType,
isBetterView, imports)) {
bestViewType = adapterViewType;
@@ -643,6 +709,76 @@ public class SetterStore {
return setterCall;
}
+ public BindingGetterCall getGetterCall(String attribute, ModelClass viewType,
+ ModelClass valueType, Map<String, String> imports) {
+ if (viewType == null) {
+ return null;
+ } else if (viewType.isViewDataBinding()) {
+ return new ViewDataBindingGetterCall(attribute);
+ }
+
+ attribute = stripNamespace(attribute);
+ viewType = viewType.erasure();
+
+ InverseMethod bestMethod = getBestGetter(viewType, valueType, attribute, imports);
+ HashMap<AccessorKey, InverseDescription> adapters = mStore.inverseAdapters.get(attribute);
+ if (adapters != null) {
+ for (AccessorKey key : adapters.keySet()) {
+ try {
+ ModelClass adapterViewType = mClassAnalyzer
+ .findClass(key.viewType, imports).erasure();
+ if (adapterViewType != null && adapterViewType.isAssignableFrom(viewType)) {
+ try {
+ L.d("getter return type is %s", key.valueType);
+ final ModelClass adapterValueType = eraseType(mClassAnalyzer
+ .findClass(key.valueType, imports));
+ L.d("getter %s returns type %s, compared to %s",
+ adapters.get(key).method, adapterValueType.toJavaCode(),
+ valueType);
+ boolean isBetterView = bestMethod.viewType == null ||
+ bestMethod.viewType.isAssignableFrom(adapterViewType);
+ if (valueType == null ||
+ isBetterParameter(adapterValueType, valueType,
+ bestMethod.returnType, isBetterView, imports)) {
+ bestMethod.viewType = adapterViewType;
+ bestMethod.returnType = adapterValueType;
+ InverseDescription inverseDescription = adapters.get(key);
+ ModelClass listenerType = ModelAnalyzer.getInstance().findClass(
+ InverseBindingListener.class);
+ BindingSetterCall eventCall = getSetterCall(
+ inverseDescription.event, viewType, listenerType, imports);
+ if (eventCall == null) {
+ List<MultiAttributeSetter> setters =
+ getMultiAttributeSetterCalls(
+ new String[]{inverseDescription.event},
+ viewType, new ModelClass[] {listenerType});
+ if (setters.size() != 1) {
+ L.e("Could not find event '%s' on View type '%s'",
+ inverseDescription.event,
+ viewType.getCanonicalName());
+ } else {
+ bestMethod.call = new AdapterGetter(inverseDescription,
+ setters.get(0));
+ }
+ } else {
+ bestMethod.call = new AdapterGetter(inverseDescription,
+ eventCall);
+ }
+ }
+
+ } catch (Exception e) {
+ L.e(e, "Unknown class: %s", key.valueType);
+ }
+ }
+ } catch (Exception e) {
+ L.e(e, "Unknown class: %s", key.viewType);
+ }
+ }
+ }
+
+ return bestMethod.call;
+ }
+
public boolean isUntaggable(String viewType) {
return mStore.untaggableTypes.containsKey(viewType);
}
@@ -691,6 +827,72 @@ public class SetterStore {
return bestMethod;
}
+ private InverseMethod getBestGetter(ModelClass viewType, ModelClass valueType,
+ String attribute, Map<String, String> imports) {
+ if (viewType.isGeneric()) {
+ if (valueType != null) {
+ valueType = eraseType(valueType, viewType.getTypeArguments());
+ }
+ viewType = viewType.erasure();
+ }
+ ModelClass bestReturnType = null;
+ InverseDescription bestDescription = null;
+ ModelClass bestViewType = null;
+ ModelMethod bestMethod = null;
+
+ HashMap<String, InverseDescription> inverseMethods = mStore.inverseMethods.get(attribute);
+ if (inverseMethods != null) {
+ for (String className : inverseMethods.keySet()) {
+ try {
+ ModelClass methodViewType = mClassAnalyzer.findClass(className, imports);
+ if (methodViewType.erasure().isAssignableFrom(viewType)) {
+ boolean isBetterViewType = bestViewType == null ||
+ bestViewType.isAssignableFrom(methodViewType);
+ final InverseDescription inverseDescription = inverseMethods.get(className);
+ final String name = inverseDescription.method.isEmpty() ?
+ trimAttributeNamespace(attribute) : inverseDescription.method;
+ ModelMethod method = methodViewType.findInstanceGetter(name);
+ ModelClass returnType = method.getReturnType(null); // no parameters
+ if (valueType == null || bestReturnType == null ||
+ isBetterParameter(returnType, valueType, bestReturnType,
+ isBetterViewType, imports)) {
+ bestDescription = inverseDescription;
+ bestReturnType = returnType;
+ bestViewType = methodViewType;
+ bestMethod = method;
+ }
+ }
+ } catch (Exception e) {
+ //printMessage(Diagnostic.Kind.NOTE, "Unknown class: " + className);
+ }
+ }
+ }
+
+ BindingGetterCall call = null;
+ if (bestDescription != null) {
+ final ModelClass listenerType = ModelAnalyzer.getInstance().findClass(
+ InverseBindingListener.class);
+ SetterCall eventSetter = getSetterCall(bestDescription.event, viewType,
+ listenerType, imports);
+ if (eventSetter == null) {
+ List<MultiAttributeSetter> setters = getMultiAttributeSetterCalls(
+ new String[] {bestDescription.event}, viewType,
+ new ModelClass[] {listenerType});
+ if (setters.size() != 1) {
+ L.e("Could not find event '%s' on View type '%s'", bestDescription.event,
+ viewType.getCanonicalName());
+ bestViewType = null;
+ bestReturnType = null;
+ } else {
+ call = new ViewGetterCall(bestDescription, bestMethod, setters.get(0));
+ }
+ } else {
+ call = new ViewGetterCall(bestDescription, bestMethod, eventSetter);
+ }
+ }
+ return new InverseMethod(call, bestReturnType, bestViewType);
+ }
+
private static ModelClass eraseType(ModelClass type, List<ModelClass> typeParameters) {
List<ModelClass> typeArguments = type.getTypeArguments();
if (typeArguments == null || typeParameters == null) {
@@ -797,20 +999,22 @@ public class SetterStore {
to.isAssignableFrom(from);
}
- private static void merge(IntermediateV1 store, Intermediate dumpStore) {
- IntermediateV1 intermediateV1 = (IntermediateV1) dumpStore.upgrade();
- merge(store.adapterMethods, intermediateV1.adapterMethods);
- merge(store.renamedMethods, intermediateV1.renamedMethods);
- merge(store.conversionMethods, intermediateV1.conversionMethods);
- store.multiValueAdapters.putAll(intermediateV1.multiValueAdapters);
- store.untaggableTypes.putAll(intermediateV1.untaggableTypes);
+ private static void merge(IntermediateV2 store, Intermediate dumpStore) {
+ IntermediateV2 intermediateV2 = (IntermediateV2) dumpStore.upgrade();
+ merge(store.adapterMethods, intermediateV2.adapterMethods);
+ merge(store.renamedMethods, intermediateV2.renamedMethods);
+ merge(store.conversionMethods, intermediateV2.conversionMethods);
+ store.multiValueAdapters.putAll(intermediateV2.multiValueAdapters);
+ store.untaggableTypes.putAll(intermediateV2.untaggableTypes);
+ merge(store.inverseAdapters, intermediateV2.inverseAdapters);
+ merge(store.inverseMethods, intermediateV2.inverseMethods);
}
- private static <K, V> void merge(HashMap<K, HashMap<V, MethodDescription>> first,
- HashMap<K, HashMap<V, MethodDescription>> second) {
+ private static <K, V, D> void merge(HashMap<K, HashMap<V, D>> first,
+ HashMap<K, HashMap<V, D>> second) {
for (K key : second.keySet()) {
- HashMap<V, MethodDescription> firstVals = first.get(key);
- HashMap<V, MethodDescription> secondVals = second.get(key);
+ HashMap<V, D> firstVals = first.get(key);
+ HashMap<V, D> secondVals = second.get(key);
if (firstVals == null) {
first.put(key, secondVals);
} else {
@@ -972,6 +1176,35 @@ public class SetterStore {
}
}
+ private static class InverseDescription extends MethodDescription {
+ private static final long serialVersionUID = 1;
+
+ public final String event;
+
+ public InverseDescription(String type, String method, String event) {
+ super(type, method);
+ this.event = event;
+ }
+
+ public InverseDescription(ExecutableElement method, String event, boolean takesComponent) {
+ super(method, 1, takesComponent);
+ this.event = event;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!super.equals(obj) || !(obj instanceof InverseDescription)) {
+ return false;
+ }
+ return event.equals(((InverseDescription) obj).event);
+ }
+
+ @Override
+ public int hashCode() {
+ return mergedHashCode(type, method, event);
+ }
+ }
+
private static class AccessorKey implements Serializable {
private static final long serialVersionUID = 1;
@@ -1027,6 +1260,24 @@ public class SetterStore {
@Override
public Intermediate upgrade() {
+ IntermediateV2 v2 = new IntermediateV2();
+ v2.adapterMethods.putAll(adapterMethods);
+ v2.renamedMethods.putAll(renamedMethods);
+ v2.conversionMethods.putAll(conversionMethods);
+ v2.untaggableTypes.putAll(untaggableTypes);
+ v2.multiValueAdapters.putAll(multiValueAdapters);
+ return v2;
+ }
+ }
+
+ private static class IntermediateV2 extends IntermediateV1 {
+ public final HashMap<String, HashMap<AccessorKey, InverseDescription>> inverseAdapters =
+ new HashMap<String, HashMap<AccessorKey, InverseDescription>>();
+ public final HashMap<String, HashMap<String, InverseDescription>> inverseMethods =
+ new HashMap<String, HashMap<String, InverseDescription>>();
+
+ @Override
+ public Intermediate upgrade() {
return this;
}
}
@@ -1344,4 +1595,206 @@ public class SetterStore {
'}';
}
}
+
+ public static class ViewDataBindingEventSetter implements BindingSetterCall {
+
+ public ViewDataBindingEventSetter() {
+ }
+
+ @Override
+ public String toJava(String componentExpression, String viewExpression,
+ String... valueExpressions) {
+ return "setBindingInverseListener(" + viewExpression + ", " +
+ valueExpressions[0] + ", " + valueExpressions[1] + ")";
+ }
+
+ @Override
+ public int getMinApi() {
+ return 0;
+ }
+
+ @Override
+ public boolean requiresOldValue() {
+ return true;
+ }
+
+ @Override
+ public ModelClass[] getParameterTypes() {
+ ModelClass[] parameterTypes = new ModelClass[1];
+ parameterTypes[0] = ModelAnalyzer.getInstance().findClass(
+ "android.databinding.ViewDataBinder.PropertyChangedInverseListener", null);
+ return parameterTypes;
+ }
+
+ @Override
+ public String getBindingAdapterInstanceClass() {
+ return null;
+ }
+ }
+
+ public interface BindingGetterCall {
+ String toJava(String componentExpression, String viewExpression);
+
+ int getMinApi();
+
+ String getBindingAdapterInstanceClass();
+
+ void setBindingAdapterCall(String method);
+
+ BindingSetterCall getEvent();
+
+ String getEventAttribute();
+ }
+
+ public static class ViewDataBindingGetterCall implements BindingGetterCall {
+ private final String mGetter;
+ private final BindingSetterCall mEventSetter;
+ private final String mAttribute;
+
+ public ViewDataBindingGetterCall(String attribute) {
+ final int colonIndex = attribute.indexOf(':');
+ mAttribute = attribute.substring(colonIndex + 1);
+ mGetter = "get" + StringUtils.capitalize(mAttribute);
+ mEventSetter = new ViewDataBindingEventSetter();
+ }
+
+ @Override
+ public String toJava(String componentExpression, String viewExpression) {
+ return viewExpression + "." + mGetter + "()";
+ }
+
+ @Override
+ public int getMinApi() {
+ return 0;
+ }
+
+ @Override
+ public String getBindingAdapterInstanceClass() {
+ return null;
+ }
+
+ @Override
+ public void setBindingAdapterCall(String method) {
+ }
+
+ @Override
+ public BindingSetterCall getEvent() {
+ return mEventSetter;
+ }
+
+ @Override
+ public String getEventAttribute() {
+ return mAttribute;
+ }
+ }
+
+ public static class ViewGetterCall implements BindingGetterCall {
+ private final InverseDescription mInverseDescription;
+ private final BindingSetterCall mEventCall;
+ private final ModelMethod mMethod;
+
+ public ViewGetterCall(InverseDescription inverseDescription, ModelMethod method,
+ BindingSetterCall eventCall) {
+ mInverseDescription = inverseDescription;
+ mEventCall = eventCall;
+ mMethod = method;
+ }
+
+ @Override
+ public BindingSetterCall getEvent() {
+ return mEventCall;
+ }
+
+ @Override
+ public String getEventAttribute() {
+ return mInverseDescription.event;
+ }
+
+ @Override
+ public String toJava(String componentExpression, String viewExpression) {
+ return viewExpression + "." + mMethod.getName() + "()";
+ }
+
+ @Override
+ public int getMinApi() {
+ return mMethod.getMinApi();
+ }
+
+ @Override
+ public String getBindingAdapterInstanceClass() {
+ return null;
+ }
+
+ @Override
+ public void setBindingAdapterCall(String method) {
+ }
+ }
+
+ public static class AdapterGetter implements BindingGetterCall {
+ private final InverseDescription mInverseDescription;
+ private String mBindingAdapterCall;
+ private final BindingSetterCall mEventCall;
+
+ public AdapterGetter(InverseDescription description, BindingSetterCall eventCall) {
+ mInverseDescription = description;
+ mEventCall = eventCall;
+ }
+
+ @Override
+ public String toJava(String componentExpression, String viewExpression) {
+ StringBuilder sb = new StringBuilder();
+
+ if (mInverseDescription.isStatic) {
+ sb.append(mInverseDescription.type);
+ } else {
+ sb.append(componentExpression).append('.').append(mBindingAdapterCall);
+ }
+ sb.append('.').append(mInverseDescription.method).append('(');
+ if (mInverseDescription.componentClass != null) {
+ if (!"DataBindingComponent".equals(mInverseDescription.componentClass)) {
+ sb.append('(').append(mInverseDescription.componentClass).append(") ");
+ }
+ sb.append(componentExpression).append(", ");
+ }
+ sb.append(viewExpression).append(')');
+ return sb.toString();
+ }
+
+ @Override
+ public int getMinApi() {
+ return 1;
+ }
+
+ @Override
+ public String getBindingAdapterInstanceClass() {
+ return mInverseDescription.isStatic ? null : mInverseDescription.type;
+ }
+
+ @Override
+ public void setBindingAdapterCall(String method) {
+ mBindingAdapterCall = method;
+ }
+
+ @Override
+ public BindingSetterCall getEvent() {
+ return mEventCall;
+ }
+
+ @Override
+ public String getEventAttribute() {
+ return mInverseDescription.event;
+ }
+ }
+
+ private static class InverseMethod {
+ public BindingGetterCall call;
+ public ModelClass returnType;
+ public ModelClass viewType;
+
+ public InverseMethod(BindingGetterCall call, ModelClass returnType, ModelClass viewType) {
+ this.call = call;
+ this.returnType = returnType;
+ this.viewType = viewType;
+ }
+ }
}
diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/KCode.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/KCode.kt
index 2ef78732..f991d405 100644
--- a/compiler/src/main/kotlin/android/databinding/tool/writer/KCode.kt
+++ b/compiler/src/main/kotlin/android/databinding/tool/writer/KCode.kt
@@ -63,7 +63,7 @@ class KCode (private val s : String? = null){
return tab(c)
}
- private fun tab(c : KCode?) : KCode {
+ fun tab(c : KCode?) : KCode {
if (c == null || isNull(c)) {
return this
}
diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt
index 7874d401..e52daf3c 100644
--- a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt
+++ b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt
@@ -14,19 +14,20 @@
package android.databinding.tool.writer
import android.databinding.tool.BindingTarget
+import android.databinding.tool.InverseBinding
import android.databinding.tool.LayoutBinder
import android.databinding.tool.expr.Expr
import android.databinding.tool.expr.ExprModel
import android.databinding.tool.expr.FieldAccessExpr
import android.databinding.tool.expr.IdentifierExpr
import android.databinding.tool.expr.ListenerExpr
-import android.databinding.tool.expr.TernaryExpr
import android.databinding.tool.expr.ResourceExpr
+import android.databinding.tool.expr.TernaryExpr
import android.databinding.tool.ext.androidId
+import android.databinding.tool.ext.br
import android.databinding.tool.ext.joinToCamelCaseAsVar
import android.databinding.tool.ext.lazyProp
import android.databinding.tool.ext.versionedLazy
-import android.databinding.tool.ext.br
import android.databinding.tool.processing.ErrorMessages
import android.databinding.tool.reflection.ModelAnalyzer
import android.databinding.tool.util.L
@@ -151,6 +152,12 @@ val Expr.fieldName by lazyProp { expr : Expr ->
expr.model.getUniqueFieldName("m${expr.readableName.capitalize()}", false)
}
+val InverseBinding.fieldName by lazyProp { inverseBinding : InverseBinding ->
+ val targetName = inverseBinding.getTarget().fieldName;
+ val eventName = inverseBinding.getEventAttribute().stripNonJava()
+ inverseBinding.getModel().getUniqueFieldName("${targetName}${eventName}", false)
+}
+
val Expr.listenerClassName by lazyProp { expr : Expr ->
expr.model.getUniqueFieldName("${expr.resolvedType.simpleName}Impl", false)
}
@@ -200,11 +207,15 @@ val Expr.conditionalFlags by lazyProp { expr : Expr ->
}
val LayoutBinder.requiredComponent by lazyProp { layoutBinder: LayoutBinder ->
- val required = layoutBinder.
+ val requiredFromBindings = layoutBinder.
bindingTargets.
flatMap { it.bindings }.
- firstOrNull { it.bindingAdapterInstanceClass != null }
- required?.bindingAdapterInstanceClass
+ firstOrNull { it.bindingAdapterInstanceClass != null }?.bindingAdapterInstanceClass
+ val requiredFromInverse = layoutBinder.
+ bindingTargets.
+ flatMap { it.inverseBindings }.
+ firstOrNull { it.bindingAdapterInstanceClass != null }?.bindingAdapterInstanceClass
+ requiredFromBindings ?: requiredFromInverse
}
fun Expr.getRequirementFlagSet(expected : Boolean) : FlagSet = conditionalFlags[if(expected) 1 else 0]
@@ -298,6 +309,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
tab(declareVariables())
tab(declareBoundValues())
tab(declareListeners())
+ tab(declareInverseBindingImpls());
tab(declareConstructor(minSdk))
tab(declareInvalidateAll())
tab(declareHasPendingBindings())
@@ -577,6 +589,10 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
tab("${mDirtyFlags.localName}$suffix |= ${flagSet.localValue(index)};")
}
} tab ("}")
+ // TODO: Remove this condition after releasing version 1.1 of SDK
+ if (ModelAnalyzer.getInstance().findClass("android.databinding.ViewDataBinding", null).isObservable) {
+ tab("notifyPropertyChanged(${it.name.br()});")
+ }
tab("super.requestRebind();")
}
nl("}")
@@ -686,6 +702,29 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
}
}
+ fun declareInverseBindingImpls() = kcode("// Inverse Binding Event Handlers") {
+ layoutBinder.getSortedTargets().filter { it.isUsed() }.forEach { target ->
+ target.getInverseBindings().forEach { inverseBinding ->
+ val className : String
+ val param : String
+ if (inverseBinding.isOnBinder()) {
+ className = "android.databinding.ViewDataBinding.PropertyChangedInverseListener"
+ param = "BR.${inverseBinding.eventAttribute}"
+ } else {
+ className = "android.databinding.InverseBindingListener"
+ param = ""
+ }
+ nl("private ${className} ${inverseBinding.fieldName} = new ${className}(${param}) {") {
+ tab("@Override")
+ tab("public void onChange() {") {
+ tab(inverseBinding.toJavaCode("mBindingComponent", mDirtyFlags)).app(";");
+ }
+ tab("}")
+ }
+ nl("};")
+ }
+ }
+ }
fun declareDirtyFlags() = kcode("// dirty flag") {
model.ext.localizedFlags.forEach { flag ->
flag.notEmpty { suffix, value ->
diff --git a/compiler/src/test/java/android/databinding/tool/LayoutBinderTest.java b/compiler/src/test/java/android/databinding/tool/LayoutBinderTest.java
index f408eb08..8b1f820d 100644
--- a/compiler/src/test/java/android/databinding/tool/LayoutBinderTest.java
+++ b/compiler/src/test/java/android/databinding/tool/LayoutBinderTest.java
@@ -14,9 +14,6 @@
package android.databinding.tool;
-import org.junit.Before;
-import org.junit.Test;
-
import android.databinding.tool.expr.Expr;
import android.databinding.tool.expr.ExprModel;
import android.databinding.tool.expr.FieldAccessExpr;
@@ -26,6 +23,9 @@ import android.databinding.tool.reflection.Callable;
import android.databinding.tool.reflection.java.JavaAnalyzer;
import android.databinding.tool.reflection.java.JavaClass;
+import org.junit.Before;
+import org.junit.Test;
+
import java.util.List;
import java.util.Map;
@@ -77,8 +77,8 @@ public class LayoutBinderTest {
int originalSize = mExprModel.size();
mLayoutBinder.addVariable("user", "android.databinding.tool2.LayoutBinderTest.TestUser",
null);
- mLayoutBinder.parse("user.name", null);
- mLayoutBinder.parse("user.lastName", null);
+ mLayoutBinder.parse("user.name", false, null);
+ mLayoutBinder.parse("user.lastName", false, null);
assertEquals(originalSize + 3, mExprModel.size());
final List<Expr> bindingExprs = mExprModel.getBindingExpressions();
assertEquals(2, bindingExprs.size());
@@ -94,7 +94,7 @@ public class LayoutBinderTest {
public void testParseWithMethods() {
mLayoutBinder.addVariable("user", "android.databinding.tool.LayoutBinderTest.TestUser",
null);
- mLayoutBinder.parse("user.fullName", null);
+ mLayoutBinder.parse("user.fullName", false, null);
Expr item = mExprModel.getBindingExpressions().get(0);
assertTrue(item instanceof FieldAccessExpr);
IdentifierExpr id = mExprModel.identifier("user");
diff --git a/compiler/src/test/java/android/databinding/tool/expr/ExprModelTest.java b/compiler/src/test/java/android/databinding/tool/expr/ExprModelTest.java
index a9ba2e92..1bcc4df9 100644
--- a/compiler/src/test/java/android/databinding/tool/expr/ExprModelTest.java
+++ b/compiler/src/test/java/android/databinding/tool/expr/ExprModelTest.java
@@ -16,12 +16,6 @@
package android.databinding.tool.expr;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
import android.databinding.Bindable;
import android.databinding.Observable;
import android.databinding.tool.LayoutBinder;
@@ -33,6 +27,12 @@ import android.databinding.tool.store.Location;
import android.databinding.tool.util.L;
import android.databinding.tool.writer.KCode;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
@@ -73,9 +73,14 @@ public class ExprModelTest {
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean full) {
return new KCode();
}
+
+ @Override
+ protected String getInvertibleError() {
+ return "DummyExpr cannot be 2-way.";
+ }
}
ExprModel mExprModel;
@@ -142,7 +147,7 @@ public class ExprModelTest {
IdentifierExpr a = lb.addVariable("a", "java.lang.String", null);
IdentifierExpr b = lb.addVariable("b", "java.lang.String", null);
IdentifierExpr c = lb.addVariable("c", "java.lang.String", null);
- lb.parse("a == null ? b : c", null);
+ lb.parse("a == null ? b : c", false, null);
mExprModel.comparison("==", a, mExprModel.symbol("null", Object.class));
lb.getModel().seal();
List<Expr> shouldRead = getShouldRead();
@@ -291,7 +296,7 @@ public class ExprModelTest {
IdentifierExpr c = lb.addVariable("c", "java.lang.String", null);
IdentifierExpr d = lb.addVariable("d", "java.lang.String", null);
IdentifierExpr e = lb.addVariable("e", "java.lang.String", null);
- final Expr aTernary = lb.parse("a == null ? b == null ? c : d : e", null);
+ final Expr aTernary = lb.parse("a == null ? b == null ? c : d : e", false, null);
assertTrue(aTernary instanceof TernaryExpr);
final Expr bTernary = ((TernaryExpr) aTernary).getIfTrue();
assertTrue(bTernary instanceof TernaryExpr);
@@ -969,7 +974,7 @@ public class ExprModelTest {
}
private <T extends Expr> T parse(LayoutBinder binder, String input, Class<T> klass) {
- final Expr parsed = binder.parse(input, null);
+ final Expr parsed = binder.parse(input, false, null);
assertTrue(klass.isAssignableFrom(parsed.getClass()));
return (T) parsed;
}
diff --git a/compiler/src/test/java/android/databinding/tool/expr/ExprTest.java b/compiler/src/test/java/android/databinding/tool/expr/ExprTest.java
index 3699402f..61d04cb6 100644
--- a/compiler/src/test/java/android/databinding/tool/expr/ExprTest.java
+++ b/compiler/src/test/java/android/databinding/tool/expr/ExprTest.java
@@ -16,9 +16,6 @@
package android.databinding.tool.expr;
-import org.junit.Before;
-import org.junit.Test;
-
import android.databinding.tool.LayoutBinder;
import android.databinding.tool.MockLayoutBinder;
import android.databinding.tool.reflection.ModelAnalyzer;
@@ -26,6 +23,9 @@ import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.java.JavaAnalyzer;
import android.databinding.tool.writer.KCode;
+import org.junit.Before;
+import org.junit.Test;
+
import java.util.BitSet;
import java.util.List;
@@ -56,11 +56,16 @@ public class ExprTest{
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean full) {
return new KCode();
}
@Override
+ protected String getInvertibleError() {
+ return null;
+ }
+
+ @Override
public boolean isDynamic() {
return true;
}
@@ -85,9 +90,14 @@ public class ExprTest{
}
@Override
- protected KCode generateCode() {
+ protected KCode generateCode(boolean full) {
return new KCode();
}
+
+ @Override
+ protected String getInvertibleError() {
+ return null;
+ }
};
expr.getUniqueKey();
}
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 5d7cb313..a2ec898b 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
@@ -40,4 +40,10 @@ public class ErrorMessages {
"elements of data bound layouts unless targeting API version 14 or greater. Value " +
"is '%s'";
public static final String SYNTAX_ERROR = "Syntax error: %s";
+ public static final String CANNOT_FIND_GETTER_CALL =
+ "Cannot find the getter for attribute '%s' with value type %s on %s.";
+ public static final String EXPRESSION_NOT_INVERTIBLE =
+ "The expression %s cannot cannot be inverted: %s";
+ public static final String TWO_WAY_EVENT_ATTRIBUTE =
+ "The attribute %s is a two-way binding event attribute and cannot be assigned.";
}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
index d9749b33..4b201319 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
@@ -13,19 +13,6 @@
package android.databinding.tool.store;
-import com.google.common.base.Strings;
-
-import org.antlr.v4.runtime.ANTLRInputStream;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.ParserRuleContext;
-import org.antlr.v4.runtime.misc.NotNull;
-import org.apache.commons.io.FileUtils;
-import org.mozilla.universalchardet.UniversalDetector;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-
import android.databinding.parser.XMLLexer;
import android.databinding.parser.XMLParser;
import android.databinding.parser.XMLParserBaseVisitor;
@@ -39,6 +26,19 @@ import android.databinding.tool.util.Preconditions;
import android.databinding.tool.util.StringUtils;
import android.databinding.tool.util.XmlEditor;
+import com.google.common.base.Strings;
+
+import org.antlr.v4.runtime.ANTLRInputStream;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.misc.NotNull;
+import org.apache.commons.io.FileUtils;
+import org.mozilla.universalchardet.UniversalDetector;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -243,9 +243,15 @@ public class LayoutFileParser {
for (XMLParser.AttributeContext attr : XmlEditor.expressionAttributes(parent)) {
String value = escapeQuotes(attr.attrValue.getText(), true);
- if (value.charAt(0) == '@' && value.charAt(1) == '{' &&
- value.charAt(value.length() - 1) == '}') {
- final String strippedValue = value.substring(2, value.length() - 1);
+ final boolean isOneWay = value.startsWith("@{");
+ final boolean isTwoWay = value.startsWith("@={");
+ if (isOneWay || isTwoWay) {
+ if (value.charAt(value.length() - 1) != '}') {
+ L.e("Expecting '}' in expression '%s'", attr.attrValue.getText());
+ }
+ final int startIndex = isTwoWay ? 3 : 2;
+ final int endIndex = value.length() - 1;
+ final String strippedValue = value.substring(startIndex, endIndex);
Location attrLocation = new Location(attr);
Location valueLocation = new Location();
// offset to 0 based
@@ -254,8 +260,8 @@ public class LayoutFileParser {
attr.attrValue.getText().indexOf(strippedValue);
valueLocation.endLine = attrLocation.endLine;
valueLocation.endOffset = attrLocation.endOffset - 2; // account for: "}
- bindingTargetBundle.addBinding(escapeQuotes(attr.attrName.getText(), false)
- , strippedValue, attrLocation, valueLocation);
+ bindingTargetBundle.addBinding(escapeQuotes(attr.attrName.getText(), false),
+ strippedValue, isTwoWay, attrLocation, valueLocation);
}
}
}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
index 3c697fb6..356698dd 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
@@ -782,8 +782,9 @@ public class ResourceBundle implements Serializable {
mLocation = location;
}
- public void addBinding(String name, String expr, Location location, Location valueLocation) {
- mBindingBundleList.add(new BindingBundle(name, expr, location, valueLocation));
+ public void addBinding(String name, String expr, boolean isTwoWay, Location location,
+ Location valueLocation) {
+ mBindingBundleList.add(new BindingBundle(name, expr, isTwoWay, location, valueLocation));
}
public void setIncludedLayout(String includedLayout) {
@@ -869,14 +870,16 @@ public class ResourceBundle implements Serializable {
private String mExpr;
private Location mLocation;
private Location mValueLocation;
+ private boolean mIsTwoWay;
public BindingBundle() {}
- public BindingBundle(String name, String expr, Location location,
+ public BindingBundle(String name, String expr, boolean isTwoWay, Location location,
Location valueLocation) {
mName = name;
mExpr = expr;
mLocation = location;
+ mIsTwoWay = isTwoWay;
mValueLocation = valueLocation;
}
@@ -898,6 +901,10 @@ public class ResourceBundle implements Serializable {
mExpr = expr;
}
+ public void setTwoWay(boolean isTwoWay) {
+ mIsTwoWay = isTwoWay;
+ }
+
@XmlElement(name="Location")
public Location getLocation() {
return mLocation;
@@ -912,6 +919,11 @@ public class ResourceBundle implements Serializable {
return mValueLocation;
}
+ @XmlElement(name="TwoWay")
+ public boolean isTwoWay() {
+ return mIsTwoWay;
+ }
+
public void setValueLocation(Location valueLocation) {
mValueLocation = valueLocation;
}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/util/L.java b/compilerCommon/src/main/java/android/databinding/tool/util/L.java
index 4a7af688..9a7e3079 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/util/L.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/util/L.java
@@ -21,6 +21,8 @@ import android.databinding.tool.processing.ScopedException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
@@ -28,7 +30,7 @@ public class L {
private static boolean sEnableDebug = false;
private static final Client sSystemClient = new Client() {
@Override
- public void printMessage(Kind kind, String message) {
+ public void printMessage(Kind kind, String message, Element element) {
if (kind == Kind.ERROR) {
System.err.println(message);
} else {
@@ -49,23 +51,33 @@ public class L {
public static void d(String msg, Object... args) {
if (sEnableDebug) {
- printMessage(Diagnostic.Kind.NOTE, String.format(msg, args));
+ printMessage(null, Diagnostic.Kind.NOTE, String.format(msg, args));
+ }
+ }
+
+ public static void d(Element element, String msg, Object... args) {
+ if (sEnableDebug) {
+ printMessage(element, Diagnostic.Kind.NOTE, String.format(msg, args));
}
}
public static void d(Throwable t, String msg, Object... args) {
if (sEnableDebug) {
- printMessage(Diagnostic.Kind.NOTE,
+ printMessage(null, Diagnostic.Kind.NOTE,
String.format(msg, args) + " " + getStackTrace(t));
}
}
public static void w(String msg, Object... args) {
- printMessage(Kind.WARNING, String.format(msg, args));
+ printMessage(null, Kind.WARNING, String.format(msg, args));
+ }
+
+ public static void w(Element element, String msg, Object... args) {
+ printMessage(element, Kind.WARNING, String.format(msg, args));
}
public static void w(Throwable t, String msg, Object... args) {
- printMessage(Kind.WARNING,
+ printMessage(null, Kind.WARNING,
String.format(msg, args) + " " + getStackTrace(t));
}
@@ -85,18 +97,24 @@ public class L {
public static void e(String msg, Object... args) {
String fullMsg = String.format(msg, args);
tryToThrowScoped(null, fullMsg);
- printMessage(Diagnostic.Kind.ERROR, fullMsg);
+ printMessage(null, Diagnostic.Kind.ERROR, fullMsg);
+ }
+
+ public static void e(Element element, String msg, Object... args) {
+ String fullMsg = String.format(msg, args);
+ tryToThrowScoped(null, fullMsg);
+ printMessage(element, Diagnostic.Kind.ERROR, fullMsg);
}
public static void e(Throwable t, String msg, Object... args) {
String fullMsg = String.format(msg, args);
tryToThrowScoped(t, fullMsg);
- printMessage(Diagnostic.Kind.ERROR,
+ printMessage(null, Diagnostic.Kind.ERROR,
fullMsg + " " + getStackTrace(t));
}
- private static void printMessage(Diagnostic.Kind kind, String message) {
- sClient.printMessage(kind, message);
+ private static void printMessage(Element element, Diagnostic.Kind kind, String message) {
+ sClient.printMessage(kind, message, element);
if (kind == Diagnostic.Kind.ERROR) {
throw new RuntimeException("failure, see logs for details.\n" + message);
}
@@ -106,8 +124,8 @@ public class L {
return sEnableDebug;
}
- public static interface Client {
- public void printMessage(Diagnostic.Kind kind, String message);
+ public interface Client {
+ void printMessage(Diagnostic.Kind kind, String message, Element element);
}
private static String getStackTrace(Throwable t) {
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 2dce2af1..e5a3da28 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/util/XmlEditor.java
@@ -16,6 +16,13 @@
package android.databinding.tool.util;
+import android.databinding.parser.BindingExpressionLexer;
+import android.databinding.parser.BindingExpressionParser;
+import android.databinding.parser.XMLLexer;
+import android.databinding.parser.XMLParser;
+import android.databinding.parser.XMLParser.AttributeContext;
+import android.databinding.parser.XMLParser.ElementContext;
+
import com.google.common.base.Joiner;
import com.google.common.xml.XmlEscapers;
@@ -25,13 +32,6 @@ import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.commons.io.FileUtils;
-import android.databinding.parser.BindingExpressionLexer;
-import android.databinding.parser.BindingExpressionParser;
-import android.databinding.parser.XMLLexer;
-import android.databinding.parser.XMLParser;
-import android.databinding.parser.XMLParser.AttributeContext;
-import android.databinding.parser.XMLParser.ElementContext;
-
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -191,16 +191,25 @@ public class XmlEditor {
List<AttributeContext> result = new ArrayList<AttributeContext>();
for (AttributeContext input : attributes(elementContext)) {
String attrName = input.attrName.getText();
- String value = input.attrValue.getText();
- if (attrName.equals("android:tag") ||
- (value.startsWith("\"@{") && value.endsWith("}\"")) ||
- (value.startsWith("'@{") && value.endsWith("}'"))) {
+ boolean isExpression = attrName.equals("android:tag");
+ if (!isExpression) {
+ final String value = input.attrValue.getText();
+ isExpression = isExpressionText(input.attrValue.getText());
+ }
+ if (isExpression) {
result.add(input);
}
}
return result;
}
+ private static boolean isExpressionText(String value) {
+ // Check if the expression ends with "}" and starts with "@{" or "@={", ignoring
+ // the surrounding quotes.
+ return (value.length() > 5 && value.charAt(value.length() - 2) == '}' &&
+ ("@{".equals(value.substring(1, 3)) || "@={".equals(value.substring(1, 4))));
+ }
+
private static Position endTagPosition(ElementContext context) {
if (context.content() == null) {
// no content, so just choose the start of the "/>"
@@ -272,8 +281,7 @@ public class XmlEditor {
} else {
// android:tag is included, regardless, so we must only count as an expression
// if android:tag has a binding expression.
- String value = expressions.get(0).attrValue.getText();
- return value.startsWith("\"@{") || value.startsWith("'@{");
+ return isExpressionText(expressions.get(0).attrValue.getText());
}
}
@@ -322,10 +330,14 @@ public class XmlEditor {
private static String defaultReplacement(XMLParser.AttributeContext attr) {
String textWithQuotes = attr.attrValue.getText();
String escapedText = textWithQuotes.substring(1, textWithQuotes.length() - 1);
- if (!escapedText.startsWith("@{") || !escapedText.endsWith("}")) {
+ final boolean isTwoWay = escapedText.startsWith("@={");
+ final boolean isOneWay = escapedText.startsWith("@{");
+ if ((!isTwoWay && !isOneWay) || !escapedText.endsWith("}")) {
return null;
}
- String text = StringUtils.unescapeXml(escapedText.substring(2, escapedText.length() - 1));
+ final int startIndex = isTwoWay ? 3 : 2;
+ final int endIndex = escapedText.length() - 1;
+ String text = StringUtils.unescapeXml(escapedText.substring(startIndex, endIndex));
ANTLRInputStream inputStream = new ANTLRInputStream(text);
BindingExpressionLexer lexer = new BindingExpressionLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);