Add RecyclerView support.

Bug: http://b.android.com/72117
Change-Id: Iba95baff59f1b715f91da25e15acf27bc052d95e
diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
index 7e4ff69..fbd5e2a 100644
--- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
+++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
@@ -22,9 +22,13 @@
 import com.android.ide.common.rendering.api.ResourceReference;
 import com.android.ide.common.rendering.api.ResourceValue;
 import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.BridgeConstants;
 import com.android.layoutlib.bridge.android.BridgeContext;
 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
+import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
+import com.android.layoutlib.bridge.android.support.RecyclerViewUtil.LayoutManagerType;
 import com.android.layoutlib.bridge.impl.ParserFactory;
+import com.android.layoutlib.bridge.impl.RenderSessionImpl;
 import com.android.resources.ResourceType;
 import com.android.util.Pair;
 
@@ -111,8 +115,7 @@
         } catch (Exception e) {
             // Wrap the real exception in a ClassNotFoundException, so that the calling method
             // can deal with it.
-            ClassNotFoundException exception = new ClassNotFoundException("onCreateView", e);
-            throw exception;
+            throw new ClassNotFoundException("onCreateView", e);
         }
 
         setupViewInContext(view, attrs);
@@ -123,7 +126,7 @@
     @Override
     public View createViewFromTag(View parent, String name, AttributeSet attrs,
             boolean inheritContext) {
-        View view = null;
+        View view;
         try {
             view = super.createViewFromTag(parent, name, attrs, inheritContext);
         } catch (InflateException e) {
@@ -134,7 +137,7 @@
                 // Wrap the real exception in an InflateException so that the calling
                 // method can deal with it.
                 InflateException exception = new InflateException();
-                if (e2.getClass().equals(ClassNotFoundException.class) == false) {
+                if (!e2.getClass().equals(ClassNotFoundException.class)) {
                     exception.initCause(e2);
                 } else {
                     exception.initCause(e);
@@ -184,7 +187,7 @@
                         return inflate(bridgeParser, root);
                     } catch (Exception e) {
                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
-                                "Failed to parse file " + f.getAbsolutePath(), e, null /*data*/);
+                                "Failed to parse file " + f.getAbsolutePath(), e, null);
 
                         return null;
                     }
@@ -194,8 +197,7 @@
         return null;
     }
 
-    private View loadCustomView(String name, AttributeSet attrs) throws ClassNotFoundException,
-            Exception{
+    private View loadCustomView(String name, AttributeSet attrs) throws Exception {
         if (mProjectCallback != null) {
             // first get the classname in case it's not the node name
             if (name.equals("view")) {
@@ -227,6 +229,20 @@
             if (viewKey != null) {
                 bc.addViewKey(view, viewKey);
             }
+            if (RenderSessionImpl.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
+                String type = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES,
+                                BridgeConstants.ATTR_LAYOUT_MANAGER_TYPE);
+                if (type != null) {
+                    LayoutManagerType layoutManagerType = LayoutManagerType.getByDisplayName(type);
+                    if (layoutManagerType == null) {
+                        Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED,
+                                "LayoutManager (" + type + ") not found, falling back to " +
+                                        "LinearLayoutManager", null);
+                    } else {
+                        bc.addCookie(view, layoutManagerType);
+                    }
+                }
+            }
         }
     }
 
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java
index eb9e7f1..fdb4567 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java
@@ -48,4 +48,7 @@
     public final static String MATCH_PARENT = "match_parent";
     public final static String FILL_PARENT = "fill_parent";
     public final static String WRAP_CONTENT = "wrap_content";
+
+    /** Attribute in the tools namespace used to specify layout manager for RecyclerView. */
+    public static final String ATTR_LAYOUT_MANAGER_TYPE = "layoutManagerType";
 }
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java
index e00ea6a..22b5192 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java
@@ -29,6 +29,8 @@
 
     public static final SessionParams.Key<String> FLAG_KEY_ROOT_TAG =
             new SessionParams.Key<String>("rootTag", String.class);
+    public static final SessionParams.Key<Boolean> FLAG_KEY_RECYCLER_VIEW_SUPPORT =
+            new SessionParams.Key<Boolean>("recyclerViewSupport", Boolean.class);
 
     // Disallow instances.
     private SessionParamsFlags() {}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java
new file mode 100644
index 0000000..2feab7a
--- /dev/null
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java
@@ -0,0 +1,206 @@
+/*
+ * 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 com.android.layoutlib.bridge.android.support;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.IProjectCallback;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.layoutlib.bridge.android.SessionParamsFlags;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+
+import static com.android.layoutlib.bridge.util.ReflectionUtils.*;
+
+/**
+ * Utility class for working with android.support.v7.widget.RecyclerView
+ */
+@SuppressWarnings("SpellCheckingInspection")  // for "recycler".
+public class RecyclerViewUtil {
+
+    /**
+     * Used by {@link LayoutManagerType}.
+     * <p/>
+     * Not declared inside the enum, since it needs to be accessible in the constructor.
+     */
+    private static final Object CONTEXT = new Object();
+
+    public static final String CN_RECYCLER_VIEW = "android.support.v7.widget.RecyclerView";
+    private static final String CN_LAYOUT_MANAGER = CN_RECYCLER_VIEW + "$LayoutManager";
+    private static final String CN_ADAPTER = CN_RECYCLER_VIEW + "$Adapter";
+
+    /**
+     * Tries to create an Adapter ({@code android.support.v7.widget.RecyclerView.Adapter} and a
+     * LayoutManager {@code RecyclerView.LayoutManager} and assign these to the {@code RecyclerView}
+     * that is passed.
+     * <p/>
+     * Any exceptions thrown during the process are logged in {@link Bridge#getLog()}
+     */
+    public static void setAdapter(@NonNull View recyclerView, @NonNull BridgeContext context,
+            @NonNull SessionParams params) {
+        try {
+            setLayoutManager(recyclerView, context, params.getProjectCallback());
+            Object adapter = createAdapter(params);
+            setProperty(recyclerView, CN_ADAPTER, adapter, "setAdapter");
+        } catch (ReflectionException e) {
+            Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+                    "Error occured while trying to setup RecyclerView.", e, null);
+        }
+    }
+
+    private static void setLayoutManager(@NonNull View recyclerView, @NonNull BridgeContext context,
+            @NonNull IProjectCallback callback) throws ReflectionException {
+        Object cookie = context.getCookie(recyclerView);
+        assert cookie == null || cookie instanceof LayoutManagerType;
+        if (cookie == null) {
+            cookie = LayoutManagerType.getDefault();
+        }
+        Object layoutManager = createLayoutManager((LayoutManagerType) cookie, context, callback);
+        setProperty(recyclerView, CN_LAYOUT_MANAGER, layoutManager, "setLayoutManager");
+    }
+
+    @Nullable
+    private static Object createLayoutManager(@Nullable LayoutManagerType type,
+            @NonNull Context context, @NonNull IProjectCallback callback)
+            throws ReflectionException {
+        if (type == null) {
+            type = LayoutManagerType.getDefault();
+        }
+        try {
+            return callback.loadView(type.getClassName(), type.getSignature(), type.getArgs(context));
+        } catch (Exception e) {
+            throw new ReflectionException(e);
+        }
+    }
+
+    @Nullable
+    private static Object createAdapter(@NonNull SessionParams params) throws ReflectionException {
+        Boolean ideSupport = params.getFlag(SessionParamsFlags.FLAG_KEY_RECYCLER_VIEW_SUPPORT);
+        if (ideSupport != Boolean.TRUE) {
+            return null;
+        }
+        try {
+            return params.getProjectCallback().loadView(CN_ADAPTER, new Class[0], new Object[0]);
+        } catch (Exception e) {
+            throw new ReflectionException(e);
+        }
+    }
+
+    private static void setProperty(@NonNull View recyclerView, @NonNull String propertyClassName,
+            @Nullable Object propertyValue, @NonNull String propertySetter)
+            throws ReflectionException {
+        if (propertyValue != null) {
+            Class<?> layoutManagerClass = getClassInstance(propertyValue, propertyClassName);
+            Method setLayoutManager = getMethod(recyclerView.getClass(),
+                    propertySetter, layoutManagerClass);
+            if (setLayoutManager != null) {
+                invoke(setLayoutManager, recyclerView, propertyValue);
+            }
+        }
+    }
+
+    /**
+     * Looks through the class hierarchy of {@code object} at runtime and returns the class matching
+     * the name {@code className}.
+     * <p/>
+     * This is used when we cannot use Class.forName() since the class we want was loaded from a
+     * different ClassLoader.
+     */
+    @NonNull
+    private static Class<?> getClassInstance(@NonNull Object object, @NonNull String className) {
+        Class<?> superClass = object.getClass();
+        while (superClass != null) {
+            if (className.equals(superClass.getName())) {
+                return superClass;
+            }
+            superClass = superClass.getSuperclass();
+        }
+        throw new RuntimeException("invalid object/classname combination.");
+    }
+
+    /** Supported LayoutManagers. */
+    public enum LayoutManagerType {
+        LINEAR_LAYOUT_MANGER("Linear",
+                "android.support.v7.widget.LinearLayoutManager",
+                new Class[]{Context.class}, new Object[]{CONTEXT}),
+        GRID_LAYOUT_MANAGER("Grid",
+                "android.support.v7.widget.GridLayoutManager",
+                new Class[]{Context.class, int.class}, new Object[]{CONTEXT, 2}),
+        STAGGERED_GRID_LAYOUT_MANAGER("StaggeredGrid",
+                "android.support.v7.widget.StaggeredGridLayoutManager",
+                new Class[]{int.class, int.class}, new Object[]{2, LinearLayout.VERTICAL});
+
+        private String mDisplayName;
+        private String mClassName;
+        private Class[] mSignature;
+        private Object[] mArgs;
+
+        private static final HashMap<String, LayoutManagerType> sDisplayNameLookup =
+                new HashMap<String, LayoutManagerType>();
+
+        static {
+            for (LayoutManagerType type : LayoutManagerType.values()) {
+                sDisplayNameLookup.put(type.mDisplayName, type);
+            }
+        }
+
+        LayoutManagerType(String displayName, String className, Class[] signature, Object[] args) {
+            mDisplayName = displayName;
+            mClassName = className;
+            mSignature = signature;
+            mArgs = args;
+        }
+
+        String getClassName() {
+            return mClassName;
+        }
+
+        Class[] getSignature() {
+            return mSignature;
+        }
+
+        @NonNull
+        Object[] getArgs(Context context) {
+            Object[] args = new Object[mArgs.length];
+            System.arraycopy(mArgs, 0, args, 0, mArgs.length);
+            for (int i = 0; i < args.length; i++) {
+                if (args[i] == CONTEXT) {
+                    args[i] = context;
+                }
+            }
+            return args;
+        }
+
+        @NonNull
+        public static LayoutManagerType getDefault() {
+            return LINEAR_LAYOUT_MANGER;
+        }
+
+        @Nullable
+        public static LayoutManagerType getByDisplayName(@Nullable String className) {
+            return sDisplayNameLookup.get(className);
+        }
+    }
+}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java
index 669e6b5..6513c5f 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java
@@ -16,8 +16,6 @@
 
 package com.android.layoutlib.bridge.impl;
 
-import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
-
 import com.android.ide.common.rendering.api.DrawableParams;
 import com.android.ide.common.rendering.api.HardwareConfig;
 import com.android.ide.common.rendering.api.ResourceValue;
@@ -38,7 +36,6 @@
 import java.awt.Color;
 import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
-import java.io.IOException;
 
 /**
  * Action to render a given Drawable provided through {@link DrawableParams#getDrawable()}.
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
index 58acab9..b6b63b1 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
@@ -23,6 +23,8 @@
 import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN;
 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
 
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
 import com.android.ide.common.rendering.api.AdapterBinding;
 import com.android.ide.common.rendering.api.HardwareConfig;
 import com.android.ide.common.rendering.api.IAnimationListener;
@@ -51,6 +53,7 @@
 import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes;
 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
 import com.android.layoutlib.bridge.android.SessionParamsFlags;
+import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
 import com.android.layoutlib.bridge.bars.BridgeActionBar;
 import com.android.layoutlib.bridge.bars.AppCompatActionBar;
 import com.android.layoutlib.bridge.bars.Config;
@@ -1327,6 +1330,8 @@
                     }
                 }
             }
+        } else if (isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
+            RecyclerViewUtil.setAdapter(view, getContext(), getParams());
         } else if (view instanceof ViewGroup) {
             ViewGroup group = (ViewGroup) view;
             final int count = group.getChildCount();
@@ -1338,6 +1343,22 @@
     }
 
     /**
+     * Check if the object is an instance of a class named {@code className}. This doesn't work
+     * for interfaces.
+     */
+    public static boolean isInstanceOf(Object object, String className) {
+        Class superClass = object.getClass();
+        while (superClass != null) {
+            String name = superClass.getName();
+            if (name.equals(className)) {
+                return true;
+            }
+            superClass = superClass.getSuperclass();
+        }
+        return false;
+    }
+
+    /**
      * Sets up a {@link TabHost} object.
      * @param tabHost the TabHost to setup.
      * @param projectCallback The project callback object to access the project R class.
@@ -1494,6 +1515,7 @@
      * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at
      *         index 1 is with the offset.
      */
+    @NonNull
     private ViewInfo[] visitContentRoot(View view, int offset, boolean setExtendedInfo) {
         ViewInfo[] result = new ViewInfo[2];
         if (view == null) {
@@ -1589,6 +1611,7 @@
      * The cookie for menu items are stored in menu item and not in the map from View stored in
      * BridgeContext.
      */
+    @Nullable
     private Object getViewKey(View view) {
         BridgeContext context = getContext();
         if (!(view instanceof MenuView.ItemView)) {
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java
new file mode 100644
index 0000000..8e61edf
--- /dev/null
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.android.layoutlib.bridge.util;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Utility to convert checked Reflection exceptions to unchecked exceptions.
+ */
+public class ReflectionUtils {
+
+    @Nullable
+    public static Method getMethod(@NonNull Class<?> clazz, @NonNull String name,
+            @Nullable Class<?>... params) throws ReflectionException {
+        try {
+            return clazz.getMethod(name, params);
+        } catch (NoSuchMethodException e) {
+            throw new ReflectionException(e);
+        }
+    }
+
+    @Nullable
+    public static Object invoke(@NonNull Method method, @Nullable Object object,
+            @Nullable Object... args) throws ReflectionException {
+        Exception ex;
+        try {
+            return method.invoke(object, args);
+        } catch (IllegalAccessException e) {
+            ex = e;
+        } catch (InvocationTargetException e) {
+            ex = e;
+        }
+        throw new ReflectionException(ex);
+    }
+
+    /**
+     * Wraps all reflection related exceptions. Created since ReflectiveOperationException was
+     * introduced in 1.7 and we are still on 1.6
+     */
+    public static class ReflectionException extends Exception {
+        public ReflectionException() {
+            super();
+        }
+
+        public ReflectionException(String message) {
+            super(message);
+        }
+
+        public ReflectionException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public ReflectionException(Throwable cause) {
+            super(cause);
+        }
+    }
+}