Improve keying for theme caches, rebase system theme on config change

Themes now use an array of applied styles rather than a String to store
their history. They are keyed based on a hash code computed from the
history of applied styles. The themed drawable cache has been abstracted
out into its own class.

Also updates system context to use DayNight as the default and ensures
that GlobalActions uses the correct context, which exercises the change.

CTS tests have been added in another CL.

Bug: 20421157
Change-Id: I9eb4b7dffd198ad24d02f656eaf0839570b59caa
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java
index 427ecce..435d5ab 100644
--- a/core/java/android/animation/AnimatorInflater.java
+++ b/core/java/android/animation/AnimatorInflater.java
@@ -108,7 +108,7 @@
             float pathErrorScale) throws NotFoundException {
         final ConfigurationBoundResourceCache<Animator> animatorCache = resources
                 .getAnimatorCache();
-        Animator animator = animatorCache.get(id, theme);
+        Animator animator = animatorCache.getInstance(id, theme);
         if (animator != null) {
             if (DBG_ANIMATOR_INFLATER) {
                 Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id));
@@ -157,7 +157,7 @@
         final ConfigurationBoundResourceCache<StateListAnimator> cache = resources
                 .getStateListAnimatorCache();
         final Theme theme = context.getTheme();
-        StateListAnimator animator = cache.get(id, theme);
+        StateListAnimator animator = cache.getInstance(id, theme);
         if (animator != null) {
             return animator;
         }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index da6d8c5..f16406a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -38,6 +38,7 @@
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.Resources.Theme;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteDebug;
 import android.database.sqlite.SQLiteDebug.DbStats;
@@ -4186,9 +4187,14 @@
             if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
                 return;
             }
-            configDiff = mConfiguration.diff(config);
-            mConfiguration.updateFrom(config);
+
+            configDiff = mConfiguration.updateFrom(config);
             config = applyCompatConfiguration(mCurDefaultDisplayDpi);
+
+            final Theme systemTheme = getSystemContext().getTheme();
+            if ((systemTheme.getChangingConfigurations() & configDiff) != 0) {
+                systemTheme.rebase();
+            }
         }
 
         ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config);
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 525059f..8d96f5c2 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -785,6 +785,7 @@
     private native final void deleteTheme(long theme);
     /*package*/ native static final void applyThemeStyle(long theme, int styleRes, boolean force);
     /*package*/ native static final void copyTheme(long dest, long source);
+    /*package*/ native static final void clearTheme(long theme);
     /*package*/ native static final int loadThemeAttributeValue(long theme, int ident,
                                                                 TypedValue outValue,
                                                                 boolean resolve);
diff --git a/core/java/android/content/res/ConfigurationBoundResourceCache.java b/core/java/android/content/res/ConfigurationBoundResourceCache.java
index cde7e84..fecda87 100644
--- a/core/java/android/content/res/ConfigurationBoundResourceCache.java
+++ b/core/java/android/content/res/ConfigurationBoundResourceCache.java
@@ -1,138 +1,58 @@
 /*
-* Copyright (C) 2014 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.content.res;
+ * Copyright (C) 2014 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.
+ */
 
-import android.util.ArrayMap;
-import android.util.LongSparseArray;
-import java.lang.ref.WeakReference;
+package android.content.res;
 
 /**
  * A Cache class which can be used to cache resource objects that are easy to clone but more
  * expensive to inflate.
- * @hide
+ *
+ * @hide For internal use only.
  */
-public class ConfigurationBoundResourceCache<T> {
-
-    private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState<T>>>> mCache =
-            new ArrayMap<String, LongSparseArray<WeakReference<ConstantState<T>>>>();
-
-    final Resources mResources;
+public class ConfigurationBoundResourceCache<T> extends ThemedResourceCache<ConstantState<T>> {
+    private final Resources mResources;
 
     /**
-     * Creates a Resource cache for the given Resources instance.
+     * Creates a cache for the given Resources instance.
      *
-     * @param resources The Resource which can be used when creating new instances.
+     * @param resources the resources to use when creating new instances
      */
     public ConfigurationBoundResourceCache(Resources resources) {
         mResources = resources;
     }
 
     /**
-     * Adds a new item to the cache.
+     * If the resource is cached, creates and returns a new instance of it.
      *
-     * @param key A custom key that uniquely identifies the resource.
-     * @param theme The Theme instance where this resource was loaded.
-     * @param constantState The constant state that can create new instances of the resource.
-     *
+     * @param key a key that uniquely identifies the drawable resource
+     * @param theme the theme where the resource will be used
+     * @return a new instance of the resource, or {@code null} if not in
+     *         the cache
      */
-    public void put(long key, Resources.Theme theme, ConstantState<T> constantState) {
-        if (constantState == null) {
-            return;
-        }
-        final String themeKey = theme == null ? "" : theme.getKey();
-        LongSparseArray<WeakReference<ConstantState<T>>> themedCache;
-        synchronized (this) {
-            themedCache = mCache.get(themeKey);
-            if (themedCache == null) {
-                themedCache = new LongSparseArray<WeakReference<ConstantState<T>>>(1);
-                mCache.put(themeKey, themedCache);
-            }
-            themedCache.put(key, new WeakReference<ConstantState<T>>(constantState));
-        }
-    }
-
-    /**
-     * If the resource is cached, creates a new instance of it and returns.
-     *
-     * @param key The long key which can be used to uniquely identify the resource.
-     * @param theme The The Theme instance where we want to load this resource.
-     *
-     * @return If this resources was loaded before, returns a new instance of it. Otherwise, returns
-     *         null.
-     */
-    public T get(long key, Resources.Theme theme) {
-        final String themeKey = theme != null ? theme.getKey() : "";
-        final LongSparseArray<WeakReference<ConstantState<T>>> themedCache;
-        final WeakReference<ConstantState<T>> wr;
-        synchronized (this) {
-            themedCache = mCache.get(themeKey);
-            if (themedCache == null) {
-                return null;
-            }
-            wr = themedCache.get(key);
-        }
-        if (wr == null) {
-            return null;
-        }
-        final ConstantState entry = wr.get();
+    public T getInstance(long key, Resources.Theme theme) {
+        final ConstantState<T> entry = get(key, theme);
         if (entry != null) {
-            return  (T) entry.newInstance(mResources, theme);
-        } else {  // our entry has been purged
-            synchronized (this) {
-                // there is a potential race condition here where this entry may be put in
-                // another thread. But we prefer it to minimize lock duration
-                themedCache.delete(key);
-            }
+            return entry.newInstance(mResources, theme);
         }
+
         return null;
     }
 
-    /**
-     * Users of ConfigurationBoundResourceCache must call this method whenever a configuration
-     * change happens. On this callback, the cache invalidates all resources that are not valid
-     * anymore.
-     *
-     * @param configChanges The configuration changes
-     */
-    public void onConfigurationChange(final int configChanges) {
-        synchronized (this) {
-            final int size = mCache.size();
-            for (int i = size - 1; i >= 0; i--) {
-                final LongSparseArray<WeakReference<ConstantState<T>>>
-                        themeCache = mCache.valueAt(i);
-                onConfigurationChangeInt(themeCache, configChanges);
-                if (themeCache.size() == 0) {
-                    mCache.removeAt(i);
-                }
-            }
-        }
+    @Override
+    public boolean shouldInvalidateEntry(ConstantState<T> entry, int configChanges) {
+        return Configuration.needNewResources(configChanges, entry.getChangingConfigurations());
     }
-
-    private void onConfigurationChangeInt(
-            final LongSparseArray<WeakReference<ConstantState<T>>> themeCache,
-            final int configChanges) {
-        final int size = themeCache.size();
-        for (int i = size - 1; i >= 0; i--) {
-            final WeakReference<ConstantState<T>> wr = themeCache.valueAt(i);
-            final ConstantState<T> constantState = wr.get();
-            if (constantState == null || Configuration.needNewResources(
-                    configChanges, constantState.getChangingConfigurations())) {
-                themeCache.removeAt(i);
-            }
-        }
-    }
-
 }
diff --git a/core/java/android/content/res/DrawableCache.java b/core/java/android/content/res/DrawableCache.java
new file mode 100644
index 0000000..fc70bc60
--- /dev/null
+++ b/core/java/android/content/res/DrawableCache.java
@@ -0,0 +1,57 @@
+/*
+ * 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.content.res;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Class which can be used to cache Drawable resources against a theme.
+ */
+class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
+    private final Resources mResources;
+
+    /**
+     * Creates a cache for the given Resources instance.
+     *
+     * @param resources the resources to use when creating new instances
+     */
+    public DrawableCache(Resources resources) {
+        mResources = resources;
+    }
+
+    /**
+     * If the resource is cached, creates and returns a new instance of it.
+     *
+     * @param key a key that uniquely identifies the drawable resource
+     * @param theme the theme where the resource will be used
+     * @return a new instance of the resource, or {@code null} if not in
+     *         the cache
+     */
+    public Drawable getInstance(long key, Resources.Theme theme) {
+        final Drawable.ConstantState entry = get(key, theme);
+        if (entry != null) {
+            return entry.newDrawable(mResources, theme);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean shouldInvalidateEntry(Drawable.ConstantState entry, int configChanges) {
+        return false;
+    }
+}
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index ae41b69..1d108a2 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -20,6 +20,8 @@
 import android.annotation.ColorInt;
 import android.annotation.StyleRes;
 import android.annotation.StyleableRes;
+
+import com.android.internal.util.GrowingArrayUtils;
 import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -132,10 +134,8 @@
     // These are protected by mAccessLock.
     private final Object mAccessLock = new Object();
     private final Configuration mTmpConfig = new Configuration();
-    private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache =
-            new ArrayMap<>();
-    private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache =
-            new ArrayMap<>();
+    private final DrawableCache mDrawableCache = new DrawableCache(this);
+    private final DrawableCache mColorDrawableCache = new DrawableCache(this);
     private final ConfigurationBoundResourceCache<ColorStateList> mColorStateListCache =
             new ConfigurationBoundResourceCache<>(this);
     private final ConfigurationBoundResourceCache<Animator> mAnimatorCache =
@@ -1441,7 +1441,7 @@
             AssetManager.applyThemeStyle(mTheme, resId, force);
 
             mThemeResId = resId;
-            mKey += Integer.toHexString(resId) + (force ? "! " : " ");
+            mKey.append(resId, force);
         }
 
         /**
@@ -1457,7 +1457,7 @@
             AssetManager.copyTheme(mTheme, other.mTheme);
 
             mThemeResId = other.mThemeResId;
-            mKey = other.mKey;
+            mKey.setTo(other.getKey());
         }
 
         /**
@@ -1765,6 +1765,9 @@
             mTheme = mAssets.createTheme();
         }
 
+        /** Unique key for the series of styles applied to this theme. */
+        private final ThemeKey mKey = new ThemeKey();
+
         @SuppressWarnings("hiding")
         private final AssetManager mAssets;
         private final long mTheme;
@@ -1772,9 +1775,6 @@
         /** Resource identifier for the theme. */
         private int mThemeResId = 0;
 
-        /** Unique key for the series of styles applied to this theme. */
-        private String mKey = "";
-
         // Needed by layoutlib.
         /*package*/ long getNativeTheme() {
             return mTheme;
@@ -1784,7 +1784,7 @@
             return mThemeResId;
         }
 
-        /*package*/ String getKey() {
+        /*package*/ ThemeKey getKey() {
             return mKey;
         }
 
@@ -1793,29 +1793,119 @@
         }
 
         /**
-         * Parses {@link #mKey} and returns a String array that holds pairs of adjacent Theme data:
-         * resource name followed by whether or not it was forced, as specified by
-         * {@link #applyStyle(int, boolean)}.
+         * Parses {@link #mKey} and returns a String array that holds pairs of
+         * adjacent Theme data: resource name followed by whether or not it was
+         * forced, as specified by {@link #applyStyle(int, boolean)}.
          *
          * @hide
          */
         @ViewDebug.ExportedProperty(category = "theme", hasAdjacentMapping = true)
         public String[] getTheme() {
-            String[] themeData = mKey.split(" ");
-            String[] themes = new String[themeData.length * 2];
-            String theme;
-            boolean forced;
-
-            for (int i = 0, j = themeData.length - 1; i < themes.length; i += 2, --j) {
-                theme = themeData[j];
-                forced = theme.endsWith("!");
-                themes[i] = forced ?
-                        getResourceNameFromHexString(theme.substring(0, theme.length() - 1)) :
-                        getResourceNameFromHexString(theme);
+            final int N = mKey.mCount;
+            final String[] themes = new String[N * 2];
+            for (int i = 0, j = N - 1; i < themes.length; i += 2, --j) {
+                final int resId = mKey.mResId[i];
+                final boolean forced = mKey.mForce[i];
+                themes[i] = getResourceName(resId);
                 themes[i + 1] = forced ? "forced" : "not forced";
             }
             return themes;
         }
+
+        /**
+         * Rebases the theme against the parent Resource object's current
+         * configuration by re-applying the styles passed to
+         * {@link #applyStyle(int, boolean)}.
+         *
+         * @hide
+         */
+        public void rebase() {
+            AssetManager.clearTheme(mTheme);
+
+            // Reapply the same styles in the same order.
+            for (int i = 0; i < mKey.mCount; i++) {
+                final int resId = mKey.mResId[i];
+                final boolean force = mKey.mForce[i];
+                AssetManager.applyThemeStyle(mTheme, resId, force);
+            }
+        }
+    }
+
+    static class ThemeKey implements Cloneable {
+        int[] mResId;
+        boolean[] mForce;
+        int mCount;
+
+        private int mHashCode = 0;
+
+        public void append(int resId, boolean force) {
+            if (mResId == null) {
+                mResId = new int[4];
+            }
+
+            if (mForce == null) {
+                mForce = new boolean[4];
+            }
+
+            mResId = GrowingArrayUtils.append(mResId, mCount, resId);
+            mForce = GrowingArrayUtils.append(mForce, mCount, force);
+            mCount++;
+
+            mHashCode = 31 * (31 * mHashCode + resId) + (force ? 1 : 0);
+        }
+
+        /**
+         * Sets up this key as a deep copy of another key.
+         *
+         * @param other the key to deep copy into this key
+         */
+        public void setTo(ThemeKey other) {
+            mResId = other.mResId == null ? null : other.mResId.clone();
+            mForce = other.mForce == null ? null : other.mForce.clone();
+            mCount = other.mCount;
+        }
+
+        @Override
+        public int hashCode() {
+            return mHashCode;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (o == null || getClass() != o.getClass() || hashCode() != o.hashCode()) {
+                return false;
+            }
+
+            final ThemeKey t = (ThemeKey) o;
+            if (mCount != t.mCount) {
+                return false;
+            }
+
+            final int N = mCount;
+            for (int i = 0; i < N; i++) {
+                if (mResId[i] != t.mResId[i] || mForce[i] != t.mForce[i]) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        /**
+         * @return a shallow copy of this key
+         */
+        @Override
+        public ThemeKey clone() {
+            final ThemeKey other = new ThemeKey();
+            other.mResId = mResId;
+            other.mForce = mForce;
+            other.mCount = mCount;
+            return other;
+        }
     }
 
     /**
@@ -1944,8 +2034,8 @@
                         + " final compat is " + mCompatibilityInfo);
             }
 
-            clearDrawableCachesLocked(mDrawableCache, configChanges);
-            clearDrawableCachesLocked(mColorDrawableCache, configChanges);
+            mDrawableCache.onConfigurationChange(configChanges);
+            mColorDrawableCache.onConfigurationChange(configChanges);
             mColorStateListCache.onConfigurationChange(configChanges);
             mAnimatorCache.onConfigurationChange(configChanges);
             mStateListAnimatorCache.onConfigurationChange(configChanges);
@@ -1983,48 +2073,6 @@
         return configChanges;
     }
 
-    private void clearDrawableCachesLocked(
-            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
-            int configChanges) {
-        final int N = caches.size();
-        for (int i = 0; i < N; i++) {
-            clearDrawableCacheLocked(caches.valueAt(i), configChanges);
-        }
-    }
-
-    private void clearDrawableCacheLocked(
-            LongSparseArray<WeakReference<ConstantState>> cache, int configChanges) {
-        if (DEBUG_CONFIG) {
-            Log.d(TAG, "Cleaning up drawables config changes: 0x"
-                    + Integer.toHexString(configChanges));
-        }
-        final int N = cache.size();
-        for (int i = 0; i < N; i++) {
-            final WeakReference<ConstantState> ref = cache.valueAt(i);
-            if (ref != null) {
-                final ConstantState cs = ref.get();
-                if (cs != null) {
-                    if (Configuration.needNewResources(
-                            configChanges, cs.getChangingConfigurations())) {
-                        if (DEBUG_CONFIG) {
-                            Log.d(TAG, "FLUSHING #0x"
-                                    + Long.toHexString(cache.keyAt(i))
-                                    + " / " + cs + " with changes: 0x"
-                                    + Integer.toHexString(cs.getChangingConfigurations()));
-                        }
-                        cache.setValueAt(i, null);
-                    } else if (DEBUG_CONFIG) {
-                        Log.d(TAG, "(Keeping #0x"
-                                + Long.toHexString(cache.keyAt(i))
-                                + " / " + cs + " with changes: 0x"
-                                + Integer.toHexString(cs.getChangingConfigurations())
-                                + ")");
-                    }
-                }
-            }
-        }
-    }
-
     /**
      * {@code Locale.toLanguageTag} will transform the obsolete (and deprecated)
      * language codes "in", "ji" and "iw" to "id", "yi" and "he" respectively.
@@ -2436,7 +2484,7 @@
         }
 
         final boolean isColorDrawable;
-        final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches;
+        final DrawableCache caches;
         final long key;
         if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                 && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
@@ -2452,7 +2500,7 @@
         // First, check whether we have a cached version of this drawable
         // that was inflated against the specified theme.
         if (!mPreloading) {
-            final Drawable cachedDrawable = getCachedDrawable(caches, key, theme);
+            final Drawable cachedDrawable = caches.getInstance(key, theme);
             if (cachedDrawable != null) {
                 return cachedDrawable;
             }
@@ -2479,13 +2527,8 @@
         // Determine if the drawable has unresolved theme attributes. If it
         // does, we'll need to apply a theme and store it in a theme-specific
         // cache.
-        final String cacheKey;
-        if (!dr.canApplyTheme()) {
-            cacheKey = CACHE_NOT_THEMED;
-        } else if (theme == null) {
-            cacheKey = CACHE_NULL_THEME;
-        } else {
-            cacheKey = theme.getKey();
+        final boolean canApplyTheme = dr.canApplyTheme();
+        if (canApplyTheme && theme != null) {
             dr = dr.mutate();
             dr.applyTheme(theme);
             dr.clearMutated();
@@ -2495,15 +2538,14 @@
         // cache: preload, not themed, null theme, or theme-specific.
         if (dr != null) {
             dr.setChangingConfigurations(value.changingConfigurations);
-            cacheDrawable(value, isColorDrawable, caches, cacheKey, key, dr);
+            cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
         }
 
         return dr;
     }
 
-    private void cacheDrawable(TypedValue value, boolean isColorDrawable,
-            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
-            String cacheKey, long key, Drawable dr) {
+    private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
+            Theme theme, boolean usesTheme, long key, Drawable dr) {
         final ConstantState cs = dr.getConstantState();
         if (cs == null) {
             return;
@@ -2531,54 +2573,12 @@
             }
         } else {
             synchronized (mAccessLock) {
-                LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(cacheKey);
-                if (themedCache == null) {
-                    // Clean out the caches before we add more. This shouldn't
-                    // happen very often.
-                    pruneCaches(caches);
-                    themedCache = new LongSparseArray<>(1);
-                    caches.put(cacheKey, themedCache);
-                }
-                themedCache.put(key, new WeakReference<>(cs));
+                caches.put(key, theme, cs, usesTheme);
             }
         }
     }
 
     /**
-     * Prunes empty caches from the cache map.
-     *
-     * @param caches The map of caches to prune.
-     */
-    private void pruneCaches(ArrayMap<String,
-            LongSparseArray<WeakReference<ConstantState>>> caches) {
-        final int N = caches.size();
-        for (int i = N - 1; i >= 0; i--) {
-            final LongSparseArray<WeakReference<ConstantState>> cache = caches.valueAt(i);
-            if (pruneCache(cache)) {
-                caches.removeAt(i);
-            }
-        }
-    }
-
-    /**
-     * Prunes obsolete weak references from a cache, returning {@code true} if
-     * the cache is empty and should be removed.
-     *
-     * @param cache The cache of weak references to prune.
-     * @return {@code true} if the cache is empty and should be removed.
-     */
-    private boolean pruneCache(LongSparseArray<WeakReference<ConstantState>> cache) {
-        final int N = cache.size();
-        for (int i = N - 1; i >= 0; i--) {
-            final WeakReference entry = cache.valueAt(i);
-            if (entry == null || entry.get() == null) {
-                cache.removeAt(i);
-            }
-        }
-        return cache.size() == 0;
-    }
-
-    /**
      * Loads a drawable from XML or resources stream.
      */
     private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
@@ -2631,51 +2631,6 @@
         return dr;
     }
 
-    private Drawable getCachedDrawable(
-            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
-            long key, Theme theme) {
-        synchronized (mAccessLock) {
-            // First search theme-agnostic cache.
-            final Drawable unthemedDrawable = getCachedDrawableLocked(
-                    caches, key, CACHE_NOT_THEMED);
-            if (unthemedDrawable != null) {
-                return unthemedDrawable;
-            }
-
-            // Next search theme-specific cache.
-            final String themeKey = theme != null ? theme.getKey() : CACHE_NULL_THEME;
-            return getCachedDrawableLocked(caches, key, themeKey);
-        }
-    }
-
-    private Drawable getCachedDrawableLocked(
-            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
-            long key, String themeKey) {
-        final LongSparseArray<WeakReference<ConstantState>> cache = caches.get(themeKey);
-        if (cache != null) {
-            final ConstantState entry = getConstantStateLocked(cache, key);
-            if (entry != null) {
-                return entry.newDrawable(this);
-            }
-        }
-        return null;
-    }
-
-    private ConstantState getConstantStateLocked(
-            LongSparseArray<WeakReference<ConstantState>> drawableCache, long key) {
-        final WeakReference<ConstantState> wr = drawableCache.get(key);
-        if (wr != null) {
-            final ConstantState entry = wr.get();
-            if (entry != null) {
-                return entry;
-            } else {
-                // Our entry has been purged.
-                drawableCache.delete(key);
-            }
-        }
-        return null;
-    }
-
     @Nullable
     ColorStateList loadColorStateList(TypedValue value, int id, Theme theme)
             throws NotFoundException {
@@ -2713,8 +2668,7 @@
         }
 
         final ConfigurationBoundResourceCache<ColorStateList> cache = mColorStateListCache;
-
-        csl = cache.get(key, theme);
+        csl = cache.getInstance(key, theme);
         if (csl != null) {
             return csl;
         }
diff --git a/core/java/android/content/res/ThemedResourceCache.java b/core/java/android/content/res/ThemedResourceCache.java
new file mode 100644
index 0000000..9a2d06147
--- /dev/null
+++ b/core/java/android/content/res/ThemedResourceCache.java
@@ -0,0 +1,233 @@
+/*
+ * 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.content.res;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources.Theme;
+import android.content.res.Resources.ThemeKey;
+import android.util.LongSparseArray;
+import android.util.ArrayMap;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Data structure used for caching data against themes.
+ *
+ * @param <T> type of data to cache
+ */
+abstract class ThemedResourceCache<T> {
+    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
+    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
+    private LongSparseArray<WeakReference<T>> mNullThemedEntries;
+
+    /**
+     * Adds a new theme-dependent entry to the cache.
+     *
+     * @param key a key that uniquely identifies the entry
+     * @param theme the theme against which this entry was inflated, or
+     *              {@code null} if the entry has no theme applied
+     * @param entry the entry to cache
+     */
+    public void put(long key, @Nullable Theme theme, @NonNull T entry) {
+        put(key, theme, entry, true);
+    }
+
+    /**
+     * Adds a new entry to the cache.
+     *
+     * @param key a key that uniquely identifies the entry
+     * @param theme the theme against which this entry was inflated, or
+     *              {@code null} if the entry has no theme applied
+     * @param entry the entry to cache
+     * @param usesTheme {@code true} if the entry is affected theme changes,
+     *                  {@code false} otherwise
+     */
+    public void put(long key, @Nullable Theme theme, @NonNull T entry, boolean usesTheme) {
+        if (entry == null) {
+            return;
+        }
+
+        synchronized (this) {
+            final LongSparseArray<WeakReference<T>> entries;
+            if (!usesTheme) {
+                entries = getUnthemedLocked(true);
+            } else {
+                entries = getThemedLocked(theme, true);
+            }
+            if (entries != null) {
+                entries.put(key, new WeakReference<>(entry));
+            }
+        }
+    }
+
+    /**
+     * Returns an entry from the cache.
+     *
+     * @param key a key that uniquely identifies the entry
+     * @param theme the theme where the entry will be used
+     * @return a cached entry, or {@code null} if not in the cache
+     */
+    @Nullable
+    public T get(long key, @Nullable Theme theme) {
+        // The themed (includes null-themed) and unthemed caches are mutually
+        // exclusive, so we'll give priority to whichever one we think we'll
+        // hit first. Since most of the framework drawables are themed, that's
+        // probably going to be the themed cache.
+        synchronized (this) {
+            final LongSparseArray<WeakReference<T>> themedEntries = getThemedLocked(theme, false);
+            if (themedEntries != null) {
+                final WeakReference<T> themedEntry = themedEntries.get(key);
+                if (themedEntry != null) {
+                    return themedEntry.get();
+                }
+            }
+
+            final LongSparseArray<WeakReference<T>> unthemedEntries = getUnthemedLocked(false);
+            if (unthemedEntries != null) {
+                final WeakReference<T> unthemedEntry = unthemedEntries.get(key);
+                if (unthemedEntry != null) {
+                    return unthemedEntry.get();
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Prunes cache entries that have been invalidated by a configuration
+     * change.
+     *
+     * @param configChanges a bitmask of configuration changes
+     */
+    public void onConfigurationChange(int configChanges) {
+        prune(configChanges);
+    }
+
+    /**
+     * Returns whether a cached entry has been invalidated by a configuration
+     * change.
+     *
+     * @param entry a cached entry
+     * @param configChanges a non-zero bitmask of configuration changes
+     * @return {@code true} if the entry is invalid, {@code false} otherwise
+     */
+    protected abstract boolean shouldInvalidateEntry(@NonNull T entry, int configChanges);
+
+    /**
+     * Returns the cached data for the specified theme, optionally creating a
+     * new entry if one does not already exist.
+     *
+     * @param t the theme for which to return cached data
+     * @param create {@code true} to create an entry if one does not already
+     *               exist, {@code false} otherwise
+     * @return the cached data for the theme, or {@code null} if the cache is
+     *         empty and {@code create} was {@code false}
+     */
+    @Nullable
+    private LongSparseArray<WeakReference<T>> getThemedLocked(@Nullable Theme t, boolean create) {
+        if (t == null) {
+            if (mNullThemedEntries == null && create) {
+                mNullThemedEntries = new LongSparseArray<>(1);
+            }
+            return mNullThemedEntries;
+        }
+
+        if (mThemedEntries == null) {
+            if (create) {
+                mThemedEntries = new ArrayMap<>(1);
+            } else {
+                return null;
+            }
+        }
+
+        final ThemeKey key = t.getKey();
+        LongSparseArray<WeakReference<T>> cache = mThemedEntries.get(key);
+        if (cache == null && create) {
+            cache = new LongSparseArray<>(1);
+
+            final ThemeKey keyClone = key.clone();
+            mThemedEntries.put(keyClone, cache);
+        }
+
+        return cache;
+    }
+
+    /**
+     * Returns the theme-agnostic cached data.
+     *
+     * @param create {@code true} to create an entry if one does not already
+     *               exist, {@code false} otherwise
+     * @return the theme-agnostic cached data, or {@code null} if the cache is
+     *         empty and {@code create} was {@code false}
+     */
+    @Nullable
+    private LongSparseArray<WeakReference<T>> getUnthemedLocked(boolean create) {
+        if (mUnthemedEntries == null && create) {
+            mUnthemedEntries = new LongSparseArray<>(1);
+        }
+        return mUnthemedEntries;
+    }
+
+    /**
+     * Prunes cache entries affected by configuration changes or where weak
+     * references have expired.
+     *
+     * @param configChanges a bitmask of configuration changes, or {@code 0} to
+     *                      simply prune missing weak references
+     * @return {@code true} if the cache is completely empty after pruning
+     */
+    private boolean prune(int configChanges) {
+        synchronized (this) {
+            if (mThemedEntries != null) {
+                for (int i = mThemedEntries.size() - 1; i >= 0; i--) {
+                    if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) {
+                        mThemedEntries.removeAt(i);
+                    }
+                }
+            }
+
+            pruneEntriesLocked(mNullThemedEntries, configChanges);
+            pruneEntriesLocked(mUnthemedEntries, configChanges);
+
+            return mThemedEntries == null && mNullThemedEntries == null
+                    && mUnthemedEntries == null;
+        }
+    }
+
+    private boolean pruneEntriesLocked(@Nullable LongSparseArray<WeakReference<T>> entries,
+            int configChanges) {
+        if (entries == null) {
+            return true;
+        }
+
+        for (int i = entries.size() - 1; i >= 0; i--) {
+            final WeakReference<T> ref = entries.valueAt(i);
+            if (ref == null || pruneEntryLocked(ref.get(), configChanges)) {
+                entries.removeAt(i);
+            }
+        }
+
+        return entries.size() == 0;
+    }
+
+    private boolean pruneEntryLocked(@Nullable T entry, int configChanges) {
+        return entry == null || (configChanges != 0
+                && shouldInvalidateEntry(entry, configChanges));
+    }
+}
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index db495dd..74a9e4e 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -976,6 +976,12 @@
     dest->setTo(*src);
 }
 
+static void android_content_AssetManager_clearTheme(JNIEnv* env, jobject clazz, jlong themeHandle)
+{
+    ResTable::Theme* theme = reinterpret_cast<ResTable::Theme*>(themeHandle);
+    theme->clear();
+}
+
 static jint android_content_AssetManager_loadThemeAttributeValue(
     JNIEnv* env, jobject clazz, jlong themeHandle, jint ident, jobject outValue, jboolean resolve)
 {
@@ -2108,6 +2114,8 @@
         (void*) android_content_AssetManager_applyThemeStyle },
     { "copyTheme", "(JJ)V",
         (void*) android_content_AssetManager_copyTheme },
+    { "clearTheme", "(J)V",
+        (void*) android_content_AssetManager_clearTheme },
     { "loadThemeAttributeValue", "(JILandroid/util/TypedValue;Z)I",
         (void*) android_content_AssetManager_loadThemeAttributeValue },
     { "getThemeChangingConfigurations", "(J)I",