Merge "Add support for AppCompat widgets"
diff --git a/bridge/src/android/content/res/BridgeTypedArray.java b/bridge/src/android/content/res/BridgeTypedArray.java
index d0e431a..1d5ac0c 100644
--- a/bridge/src/android/content/res/BridgeTypedArray.java
+++ b/bridge/src/android/content/res/BridgeTypedArray.java
@@ -584,6 +584,7 @@
         if (value == null) {
             return defValue;
         }
+        value = value.trim();
 
         // if the value is just an integer, return it.
         try {
@@ -595,6 +596,11 @@
             // pass
         }
 
+        if (value.startsWith("#")) {
+            // this looks like a color, do not try to parse it
+            return defValue;
+        }
+
         // Handle the @id/<name>, @+id/<name> and @android:id/<name>
         // We need to return the exact value that was compiled (from the various R classes),
         // as these values can be reused internally with calls to findViewById().
diff --git a/bridge/src/android/view/BridgeInflater.java b/bridge/src/android/view/BridgeInflater.java
index f38fb19..3667f58 100644
--- a/bridge/src/android/view/BridgeInflater.java
+++ b/bridge/src/android/view/BridgeInflater.java
@@ -42,9 +42,24 @@
 import android.widget.NumberPicker;
 
 import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
+import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
+import static com.android.SdkConstants.BUTTON;
+import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
+import static com.android.SdkConstants.CHECK_BOX;
+import static com.android.SdkConstants.EDIT_TEXT;
+import static com.android.SdkConstants.IMAGE_BUTTON;
+import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
+import static com.android.SdkConstants.RADIO_BUTTON;
+import static com.android.SdkConstants.SEEK_BAR;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.TEXT_VIEW;
 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
 
 /**
@@ -53,6 +68,13 @@
 public final class BridgeInflater extends LayoutInflater {
 
     private final LayoutlibCallback mLayoutlibCallback;
+    /**
+     * If true, the inflater will try to replace the framework widgets with the AppCompat versions.
+     * Ideally, this should be based on the activity being an AppCompat activity but since that is
+     * not trivial to check from layoutlib, we currently base the decision on the current theme
+     * being an AppCompat theme.
+     */
+    private boolean mLoadAppCompatViews;
     private boolean mIsInMerge = false;
     private ResourceReference mResourceReference;
     private Map<View, String> mOpenDrawerLayouts;
@@ -60,6 +82,15 @@
     // Keep in sync with the same value in LayoutInflater.
     private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
 
+    private static final String APPCOMPAT_WIDGET_PREFIX = "android.support.v7.widget.AppCompat";
+    /** List of platform widgets that have an AppCompat version */
+    private static final Set<String> APPCOMPAT_VIEWS = Collections.unmodifiableSet(
+            new HashSet<>(
+                    Arrays.asList(TEXT_VIEW, "ImageSwitcher", BUTTON, EDIT_TEXT, SPINNER,
+                            IMAGE_BUTTON, CHECK_BOX, RADIO_BUTTON, CHECKED_TEXT_VIEW,
+                            AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW, "RatingBar",
+                            SEEK_BAR)));
+
     /**
      * List of class prefixes which are tried first by default.
      * <p/>
@@ -75,13 +106,15 @@
         return sClassPrefixList;
     }
 
-    protected BridgeInflater(LayoutInflater original, Context newContext) {
+    private BridgeInflater(LayoutInflater original, Context newContext) {
         super(original, newContext);
         newContext = getBaseContext(newContext);
         if (newContext instanceof BridgeContext) {
             mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback();
+            mLoadAppCompatViews = ((BridgeContext) newContext).isAppCompatTheme();
         } else {
             mLayoutlibCallback = null;
+            mLoadAppCompatViews = false;
         }
     }
 
@@ -91,10 +124,11 @@
      * @param context The Android application context.
      * @param layoutlibCallback the {@link LayoutlibCallback} object.
      */
-    public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) {
+    public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
         super(context);
         mLayoutlibCallback = layoutlibCallback;
         mConstructorArgs[0] = context;
+        mLoadAppCompatViews = context.isAppCompatTheme();
     }
 
     @Override
@@ -102,28 +136,38 @@
         View view = null;
 
         try {
-            // First try to find a class using the default Android prefixes
-            for (String prefix : sClassPrefixList) {
+            if (mLoadAppCompatViews && APPCOMPAT_VIEWS.contains(name)) {
+                // We are using an AppCompat theme so try to load the appcompat views
+                view = loadCustomView(APPCOMPAT_WIDGET_PREFIX + name, attrs);
+
+                if (view == null) {
+                    mLoadAppCompatViews = false; // Do not try anymore
+                }
+            } else {
+
+                // First try to find a class using the default Android prefixes
+                for (String prefix : sClassPrefixList) {
+                    try {
+                        view = createView(name, prefix, attrs);
+                        if (view != null) {
+                            break;
+                        }
+                    } catch (ClassNotFoundException e) {
+                        // Ignore. We'll try again using the base class below.
+                    }
+                }
+
+                // Next try using the parent loader. This will most likely only work for
+                // fully-qualified class names.
                 try {
-                    view = createView(name, prefix, attrs);
-                    if (view != null) {
-                        break;
+                    if (view == null) {
+                        view = super.onCreateView(name, attrs);
                     }
                 } catch (ClassNotFoundException e) {
-                    // Ignore. We'll try again using the base class below.
+                    // Ignore. We'll try again using the custom view loader below.
                 }
             }
 
-            // Next try using the parent loader. This will most likely only work for
-            // fully-qualified class names.
-            try {
-                if (view == null) {
-                    view = super.onCreateView(name, attrs);
-                }
-            } catch (ClassNotFoundException e) {
-                // Ignore. We'll try again using the custom view loader below.
-            }
-
             // Finally try again using the custom view loader
             if (view == null) {
                 view = loadCustomView(name, attrs);
diff --git a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 194eecc..72ac4c3 100644
--- a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -110,6 +110,7 @@
  */
 @SuppressWarnings("deprecation")  // For use of Pair.
 public final class BridgeContext extends Context {
+    private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
 
     /** The map adds cookies to each view so that IDE can link xml tags to views. */
     private final HashMap<View, Object> mViewKeyMap = new HashMap<>();
@@ -153,6 +154,7 @@
     private ClassLoader mClassLoader;
     private IBinder mBinder;
     private PackageManager mPackageManager;
+    private Boolean mIsThemeAppCompat;
 
     /**
      * Some applications that target both pre API 17 and post API 17, set the newer attrs to
@@ -479,6 +481,36 @@
         return Pair.of(null, Boolean.FALSE);
     }
 
+    /**
+     * Returns whether the current selected theme is based on AppCompat
+     */
+    public boolean isAppCompatTheme() {
+        // If a cached value exists, return it.
+        if (mIsThemeAppCompat != null) {
+            return mIsThemeAppCompat;
+        }
+        // Ideally, we should check if the corresponding activity extends
+        // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
+        StyleResourceValue defaultTheme = mRenderResources.getDefaultTheme();
+        // We can't simply check for parent using resources.themeIsParentOf() since the
+        // inheritance structure isn't really what one would expect. The first common parent
+        // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
+        boolean isThemeAppCompat = false;
+        for (int i = 0; i < 50; i++) {
+            if (defaultTheme == null) {
+                break;
+            }
+            // for loop ensures that we don't run into cyclic theme inheritance.
+            if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
+                isThemeAppCompat = true;
+                break;
+            }
+            defaultTheme = mRenderResources.getParent(defaultTheme);
+        }
+        mIsThemeAppCompat = isThemeAppCompat;
+        return isThemeAppCompat;
+    }
+
     @SuppressWarnings("deprecation")
     private ILayoutPullParser getParser(ResourceReference resource) {
         ILayoutPullParser parser;
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
index 1afd90d..537fa77 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
@@ -20,7 +20,6 @@
 import com.android.ide.common.rendering.api.RenderResources;
 import com.android.ide.common.rendering.api.ResourceValue;
 import com.android.ide.common.rendering.api.SessionParams;
-import com.android.ide.common.rendering.api.StyleResourceValue;
 import com.android.layoutlib.bridge.Bridge;
 import com.android.layoutlib.bridge.android.BridgeContext;
 import com.android.layoutlib.bridge.android.RenderParamsFlags;
@@ -94,7 +93,6 @@
     private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize";
     private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT;
     private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT;
-    private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
 
     // Default sizes
     private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
@@ -236,7 +234,7 @@
         boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
 
         BridgeActionBar actionBar;
-        if (mBuilder.isThemeAppCompat() && !isMenu) {
+        if (context.isAppCompatTheme() && !isMenu) {
             actionBar = new AppCompatActionBar(context, params);
         } else {
             actionBar = new FrameworkActionBar(context, params);
@@ -324,8 +322,6 @@
         private boolean mTranslucentStatus;
         private boolean mTranslucentNav;
 
-        private Boolean mIsThemeAppCompat;
-
         public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
             mParams = params;
             mContext = context;
@@ -365,7 +361,7 @@
             }
             // Check if an actionbar is needed
             boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR,
-                    !isThemeAppCompat(), true);
+                    !mContext.isAppCompatTheme(), true);
             if (windowActionBar) {
                 mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
             } else {
@@ -420,33 +416,6 @@
             return mParams.getHardwareConfig().hasSoftwareButtons();
         }
 
-        private boolean isThemeAppCompat() {
-            // If a cached value exists, return it.
-            if (mIsThemeAppCompat != null) {
-                return mIsThemeAppCompat;
-            }
-            // Ideally, we should check if the corresponding activity extends
-            // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
-            StyleResourceValue defaultTheme = mResources.getDefaultTheme();
-            // We can't simply check for parent using resources.themeIsParentOf() since the
-            // inheritance structure isn't really what one would expect. The first common parent
-            // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
-            boolean isThemeAppCompat = false;
-            for (int i = 0; i < 50; i++) {
-                if (defaultTheme == null) {
-                    break;
-                }
-                // for loop ensures that we don't run into cyclic theme inheritance.
-                if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
-                    isThemeAppCompat = true;
-                    break;
-                }
-                defaultTheme = mResources.getParent(defaultTheme);
-            }
-            mIsThemeAppCompat = isThemeAppCompat;
-            return isThemeAppCompat;
-        }
-
         /**
          * Return true if the status bar or nav bar are present, they are not translucent (i.e
          * content doesn't overlap with them).
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
index a21de56..c197e40 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
@@ -77,6 +77,7 @@
      */
     public static int getColor(String value) {
         if (value != null) {
+            value = value.trim();
             if (!value.startsWith("#")) {
                 if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) {
                     throw new NumberFormatException(String.format(