Merge "Prototype XML view compiler"
am: 956791cee8

Change-Id: I879bfc0b0755b22c1653a3d06dedc890c6c4b8a6
diff --git a/startop/tools/view_compiler/Android.bp b/startop/tools/view_compiler/Android.bp
new file mode 100644
index 0000000..c3e9184
--- /dev/null
+++ b/startop/tools/view_compiler/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_host_static {
+    name: "libviewcompiler",
+    srcs: [
+        "java_lang_builder.cc",
+        "util.cc",
+    ],
+    static_libs: [
+        "libbase"
+    ]
+}
+
+cc_binary_host {
+    name: "viewcompiler",
+    srcs: [
+        "main.cc",
+    ],
+    static_libs: [
+        "libbase",
+        "libtinyxml2",
+        "libgflags",
+        "libviewcompiler",
+    ],
+}
+
+cc_test_host {
+    name: "view-compiler-tests",
+    srcs: [
+        "util_test.cc",
+    ],
+    static_libs: [
+        "libviewcompiler",
+    ]
+}
diff --git a/startop/tools/view_compiler/README.md b/startop/tools/view_compiler/README.md
new file mode 100644
index 0000000..5659501
--- /dev/null
+++ b/startop/tools/view_compiler/README.md
@@ -0,0 +1,25 @@
+# View Compiler
+
+This directory contains an experimental compiler for layout files.
+
+It will take a layout XML file and produce a CompiledLayout.java file with a
+specialized layout inflation function.
+
+To use it, let's assume you had a layout in `my_layout.xml` and your app was in
+the Java language package `com.example.myapp`. Run the following command:
+
+    viewcompiler my_layout.xml --package com.example.myapp --out CompiledView.java
+
+This will produce a `CompiledView.java`, which can then be compiled into your
+Android app. Then to use it, in places where you would have inflated
+`R.layouts.my_layout`, instead call `CompiledView.inflate`.
+
+Precompiling views like this generally improves the time needed to inflate them.
+
+This tool is still in its early stages and has a number of limitations.
+* Currently only one layout can be compiled at a time.
+* `merge` and `include` nodes are not supported.
+* View compilation is a manual process that requires code changes in the
+  application.
+* This only works for apps that do not use a custom layout inflater.
+* Other limitations yet to be discovered.
diff --git a/startop/tools/view_compiler/TEST_MAPPING b/startop/tools/view_compiler/TEST_MAPPING
new file mode 100644
index 0000000..cc4b17a
--- /dev/null
+++ b/startop/tools/view_compiler/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "view-compiler-tests"
+    }
+  ]
+}
diff --git a/startop/tools/view_compiler/java_lang_builder.cc b/startop/tools/view_compiler/java_lang_builder.cc
new file mode 100644
index 0000000..0b8754f
--- /dev/null
+++ b/startop/tools/view_compiler/java_lang_builder.cc
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "java_lang_builder.h"
+
+#include "android-base/stringprintf.h"
+
+using android::base::StringPrintf;
+using std::string;
+
+void JavaLangViewBuilder::Start() const {
+  out_ << StringPrintf("package %s;\n", package_.c_str())
+       << "import android.content.Context;\n"
+          "import android.content.res.Resources;\n"
+          "import android.content.res.XmlResourceParser;\n"
+          "import android.util.AttributeSet;\n"
+          "import android.util.Xml;\n"
+          "import android.view.*;\n"
+          "import android.widget.*;\n"
+          "\n"
+          "public final class CompiledView {\n"
+          "\n"
+          "static <T extends View> T createView(Context context, AttributeSet attrs, View parent, "
+          "String name, LayoutInflater.Factory factory, LayoutInflater.Factory2 factory2) {"
+          "\n"
+          "  if (factory2 != null) {\n"
+          "    return (T)factory2.onCreateView(parent, name, context, attrs);\n"
+          "  } else if (factory != null) {\n"
+          "    return (T)factory.onCreateView(name, context, attrs);\n"
+          "  }\n"
+          // TODO: find a way to call the private factory
+          "  return null;\n"
+          "}\n"
+          "\n"
+          "  public static View inflate(Context context) {\n"
+          "    try {\n"
+          "      LayoutInflater inflater = LayoutInflater.from(context);\n"
+          "      LayoutInflater.Factory factory = inflater.getFactory();\n"
+          "      LayoutInflater.Factory2 factory2 = inflater.getFactory2();\n"
+          "      Resources res = context.getResources();\n"
+       << StringPrintf("      XmlResourceParser xml = res.getLayout(%s.R.layout.%s);\n",
+                       package_.c_str(),
+                       layout_name_.c_str())
+       << "      AttributeSet attrs = Xml.asAttributeSet(xml);\n"
+          // The Java-language XmlPullParser needs a call to next to find the start document tag.
+          "      xml.next(); // start document\n";
+}
+
+void JavaLangViewBuilder::Finish() const {
+  out_ << "    } catch (Exception e) {\n"
+          "      return null;\n"
+          "    }\n"  // end try
+          "  }\n"    // end inflate
+          "}\n";     // end CompiledView
+}
+
+void JavaLangViewBuilder::StartView(const string& class_name) {
+  const string view_var = MakeVar("view");
+  const string layout_var = MakeVar("layout");
+  std::string parent = "null";
+  if (!view_stack_.empty()) {
+    const StackEntry& parent_entry = view_stack_.back();
+    parent = parent_entry.view_var;
+  }
+  out_ << "      xml.next(); // <" << class_name << ">\n"
+       << StringPrintf("      %s %s = createView(context, attrs, %s, \"%s\", factory, factory2);\n",
+                       class_name.c_str(),
+                       view_var.c_str(),
+                       parent.c_str(),
+                       class_name.c_str())
+       << StringPrintf("      if (%s == null) %s = new %s(context, attrs);\n",
+                       view_var.c_str(),
+                       view_var.c_str(),
+                       class_name.c_str());
+  if (!view_stack_.empty()) {
+    out_ << StringPrintf("      ViewGroup.LayoutParams %s = %s.generateLayoutParams(attrs);\n",
+                         layout_var.c_str(),
+                         parent.c_str());
+  }
+  view_stack_.push_back({class_name, view_var, layout_var});
+}
+
+void JavaLangViewBuilder::FinishView() {
+  const StackEntry var = view_stack_.back();
+  view_stack_.pop_back();
+  if (!view_stack_.empty()) {
+    const string& parent = view_stack_.back().view_var;
+    out_ << StringPrintf("      xml.next(); // </%s>\n", var.class_name.c_str())
+         << StringPrintf("      %s.addView(%s, %s);\n",
+                         parent.c_str(),
+                         var.view_var.c_str(),
+                         var.layout_params_var.c_str());
+  } else {
+    out_ << StringPrintf("      return %s;\n", var.view_var.c_str());
+  }
+}
+
+const std::string JavaLangViewBuilder::MakeVar(std::string prefix) {
+  std::stringstream v;
+  v << prefix << view_id_++;
+  return v.str();
+}
diff --git a/startop/tools/view_compiler/java_lang_builder.h b/startop/tools/view_compiler/java_lang_builder.h
new file mode 100644
index 0000000..c8d20b2
--- /dev/null
+++ b/startop/tools/view_compiler/java_lang_builder.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef JAVA_LANG_BUILDER_H_
+#define JAVA_LANG_BUILDER_H_
+
+#include <iostream>
+#include <sstream>
+#include <vector>
+
+// Build Java language code to instantiate views.
+//
+// This has a very small interface to make it easier to generate additional
+// backends, such as a direct-to-DEX version.
+class JavaLangViewBuilder {
+ public:
+  JavaLangViewBuilder(std::string package, std::string layout_name, std::ostream& out = std::cout)
+      : package_(package), layout_name_(layout_name), out_(out) {}
+
+  // Begin generating a class. Adds the package boilerplate, etc.
+  void Start() const;
+  // Finish generating a class, closing off any open curly braces, etc.
+  void Finish() const;
+
+  // Begin creating a view (i.e. process the opening tag)
+  void StartView(const std::string& class_name);
+  // Finish a view, after all of its child nodes have been processed.
+  void FinishView();
+
+ private:
+  const std::string MakeVar(std::string prefix);
+
+  std::string const package_;
+  std::string const layout_name_;
+
+  std::ostream& out_;
+
+  size_t view_id_ = 0;
+
+  struct StackEntry {
+      // The class name for this view object
+      const std::string class_name;
+
+      // The variable name that is holding the view object
+      const std::string view_var;
+
+      // The variable name that holds the object's layout parameters
+      const std::string layout_params_var;
+  };
+  std::vector<StackEntry> view_stack_;
+};
+
+#endif  // JAVA_LANG_BUILDER_H_
diff --git a/startop/tools/view_compiler/main.cc b/startop/tools/view_compiler/main.cc
new file mode 100644
index 0000000..0ad7e24
--- /dev/null
+++ b/startop/tools/view_compiler/main.cc
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "gflags/gflags.h"
+
+#include "java_lang_builder.h"
+#include "util.h"
+
+#include "tinyxml2.h"
+
+#include <fstream>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace tinyxml2;
+using std::string;
+
+constexpr char kStdoutFilename[]{"stdout"};
+
+DEFINE_string(package, "", "The package name for the generated class (required)");
+DEFINE_string(out, kStdoutFilename, "Where to write the generated class");
+
+namespace {
+class ViewCompilerXmlVisitor : public XMLVisitor {
+ public:
+  ViewCompilerXmlVisitor(JavaLangViewBuilder* builder) : builder_(builder) {}
+
+  bool VisitEnter(const XMLDocument& /*doc*/) override {
+    builder_->Start();
+    return true;
+  }
+
+  bool VisitExit(const XMLDocument& /*doc*/) override {
+    builder_->Finish();
+    return true;
+  }
+
+  bool VisitEnter(const XMLElement& element, const XMLAttribute* /*firstAttribute*/) override {
+    builder_->StartView(element.Name());
+    return true;
+  }
+
+  bool VisitExit(const XMLElement& /*element*/) override {
+    builder_->FinishView();
+    return true;
+  }
+
+ private:
+  JavaLangViewBuilder* builder_;
+};
+}  // end namespace
+
+int main(int argc, char** argv) {
+  constexpr size_t kProgramName = 0;
+  constexpr size_t kFileNameParam = 1;
+  constexpr size_t kNumRequiredArgs = 2;
+
+  gflags::SetUsageMessage(
+      "Compile XML layout files into equivalent Java language code\n"
+      "\n"
+      "  example usage:  viewcompiler layout.xml --package com.example.androidapp");
+  gflags::ParseCommandLineFlags(&argc, &argv, /*remove_flags*/ true);
+
+  gflags::CommandLineFlagInfo cmd = gflags::GetCommandLineFlagInfoOrDie("package");
+  if (argc != kNumRequiredArgs || cmd.is_default) {
+    gflags::ShowUsageWithFlags(argv[kProgramName]);
+    return 1;
+  }
+
+  const char* const filename = argv[kFileNameParam];
+  const string layout_name = FindLayoutNameFromFilename(filename);
+
+  // We want to generate Java language code to inflate exactly this layout. This means
+  // generating code to walk the resource XML too.
+
+  XMLDocument xml;
+  xml.LoadFile(filename);
+
+  std::ofstream outfile;
+  if (FLAGS_out != kStdoutFilename) {
+    outfile.open(FLAGS_out);
+  }
+  JavaLangViewBuilder builder{
+      FLAGS_package, layout_name, FLAGS_out == kStdoutFilename ? std::cout : outfile};
+
+  ViewCompilerXmlVisitor visitor{&builder};
+  xml.Accept(&visitor);
+
+  return 0;
+}
\ No newline at end of file
diff --git a/startop/tools/view_compiler/util.cc b/startop/tools/view_compiler/util.cc
new file mode 100644
index 0000000..69df41d
--- /dev/null
+++ b/startop/tools/view_compiler/util.cc
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "util.h"
+
+using std::string;
+
+// TODO: see if we can borrow this from somewhere else, like aapt2.
+string FindLayoutNameFromFilename(const string& filename) {
+  size_t start = filename.rfind("/");
+  if (start == string::npos) {
+    start = 0;
+  } else {
+    start++;  // advance past '/' character
+  }
+  size_t end = filename.find(".", start);
+
+  return filename.substr(start, end - start);
+}
diff --git a/startop/tools/view_compiler/util.h b/startop/tools/view_compiler/util.h
new file mode 100644
index 0000000..03e0939
--- /dev/null
+++ b/startop/tools/view_compiler/util.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef UTIL_H_
+#define UTIL_H_
+
+#include <string>
+
+std::string FindLayoutNameFromFilename(const std::string& filename);
+
+#endif  // UTIL_H_
diff --git a/startop/tools/view_compiler/util_test.cc b/startop/tools/view_compiler/util_test.cc
new file mode 100644
index 0000000..d1540d3
--- /dev/null
+++ b/startop/tools/view_compiler/util_test.cc
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "util.h"
+
+#include "gtest/gtest.h"
+
+using std::string;
+
+TEST(UtilTest, FindLayoutNameFromFilename) {
+  EXPECT_EQ("bar", ::FindLayoutNameFromFilename("foo/bar.xml"));
+  EXPECT_EQ("bar", ::FindLayoutNameFromFilename("bar.xml"));
+  EXPECT_EQ("bar", ::FindLayoutNameFromFilename("./foo/bar.xml"));
+  EXPECT_EQ("bar", ::FindLayoutNameFromFilename("/foo/bar.xml"));
+}