Add ResourceLoader API with .apk and .arsc support

ResourceLoaders allow inserting another .apk/.arsc into AssetManager's
resource resolution search. The effect is similar to overlays,
where a entry of >= config later in the path list will return that
ApkAsset's resource value instead.

Because loading from an .arsc is supported, which doesn't contain
any actual files, ResourceLoader exposes loadDrawable and
loadXmlResourceParser to allow an application load those files from
anywhere or create them in code.

The data being loaded is either pushed into an .apk or .arsc that
mocks itself as the package being "overlaid" and is passed in
through ResourcesProvider, an interface with static methods that
supports loading from a readable path on disk or a FileDescriptor.

The APIs are accessed through a Context's getResources(), which
has been changed to be unique per "Context-scope", which is usually
the lifetime of the Java object. The exception is that Activities
who get their Resources object persisted across recreations
maintain that logic for persisting ResourceLoaders.

Bug: 135270223

Test: atest FrameworksResourceLoaderTests

Change-Id: I6929f0828629ad39a21fa155e7fec73bd75eec7d
diff --git a/api/current.txt b/api/current.txt
index 677e8c2..b974059 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -12402,6 +12402,8 @@
 
   public class Resources {
     ctor @Deprecated public Resources(android.content.res.AssetManager, android.util.DisplayMetrics, android.content.res.Configuration);
+    method public void addLoader(@NonNull android.content.res.loader.ResourceLoader, @NonNull android.content.res.loader.ResourcesProvider, @IntRange(from=0) int);
+    method public int addLoader(@NonNull android.content.res.loader.ResourceLoader, @NonNull android.content.res.loader.ResourcesProvider);
     method public final void finishPreloading();
     method public final void flushLayoutCache();
     method @NonNull public android.content.res.XmlResourceParser getAnimation(@AnimatorRes @AnimRes int) throws android.content.res.Resources.NotFoundException;
@@ -12428,6 +12430,7 @@
     method @NonNull public int[] getIntArray(@ArrayRes int) throws android.content.res.Resources.NotFoundException;
     method public int getInteger(@IntegerRes int) throws android.content.res.Resources.NotFoundException;
     method @NonNull public android.content.res.XmlResourceParser getLayout(@LayoutRes int) throws android.content.res.Resources.NotFoundException;
+    method @NonNull public java.util.List<android.util.Pair<android.content.res.loader.ResourceLoader,android.content.res.loader.ResourcesProvider>> getLoaders();
     method @Deprecated public android.graphics.Movie getMovie(@RawRes int) throws android.content.res.Resources.NotFoundException;
     method @NonNull public String getQuantityString(@PluralsRes int, int, java.lang.Object...) throws android.content.res.Resources.NotFoundException;
     method @NonNull public String getQuantityString(@PluralsRes int, int) throws android.content.res.Resources.NotFoundException;
@@ -12455,6 +12458,8 @@
     method public android.content.res.AssetFileDescriptor openRawResourceFd(@RawRes int) throws android.content.res.Resources.NotFoundException;
     method public void parseBundleExtra(String, android.util.AttributeSet, android.os.Bundle) throws org.xmlpull.v1.XmlPullParserException;
     method public void parseBundleExtras(android.content.res.XmlResourceParser, android.os.Bundle) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public int removeLoader(@NonNull android.content.res.loader.ResourceLoader);
+    method public void setLoaders(@Nullable java.util.List<android.util.Pair<android.content.res.loader.ResourceLoader,android.content.res.loader.ResourcesProvider>>);
     method @Deprecated public void updateConfiguration(android.content.res.Configuration, android.util.DisplayMetrics);
     field @AnyRes public static final int ID_NULL = 0; // 0x0
   }
@@ -12522,6 +12527,33 @@
 
 }
 
+package android.content.res.loader {
+
+  public class DirectoryResourceLoader implements android.content.res.loader.ResourceLoader {
+    ctor public DirectoryResourceLoader(@NonNull java.io.File);
+    method @Nullable public java.io.File findFile(@NonNull String);
+    method @NonNull public java.io.File getDirectory();
+  }
+
+  public interface ResourceLoader {
+    method @Nullable public default java.io.InputStream loadAsset(@NonNull String, int) throws java.io.IOException;
+    method @Nullable public default android.os.ParcelFileDescriptor loadAssetFd(@NonNull String) throws java.io.IOException;
+    method @Nullable public default android.graphics.drawable.Drawable loadDrawable(@NonNull android.util.TypedValue, int, int, @Nullable android.content.res.Resources.Theme);
+    method @Nullable public default android.content.res.XmlResourceParser loadXmlResourceParser(@NonNull String, @AnyRes int);
+  }
+
+  public final class ResourcesProvider implements java.lang.AutoCloseable java.io.Closeable {
+    method public void close();
+    method @NonNull public static android.content.res.loader.ResourcesProvider empty();
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromApk(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromApk(@NonNull android.os.SharedMemory) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromArsc(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromArsc(@NonNull android.os.SharedMemory) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromSplit(@NonNull android.content.Context, @NonNull String) throws java.io.IOException;
+  }
+
+}
+
 package android.database {
 
   public abstract class AbstractCursor implements android.database.CrossProcessCursor {
diff --git a/api/test-current.txt b/api/test-current.txt
index 47854059..804001d 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -788,7 +788,9 @@
 
   public final class AssetManager implements java.lang.AutoCloseable {
     method @NonNull public String[] getApkPaths();
+    method @Nullable public String getLastResourceResolution();
     method @Nullable public String getOverlayablesToString(String);
+    method public void setResourceResolutionLoggingEnabled(boolean);
   }
 
   public final class Configuration implements java.lang.Comparable<android.content.res.Configuration> android.os.Parcelable {
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index cb9ebac..9e6054c 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -32,6 +32,7 @@
 import android.content.res.Resources;
 import android.content.res.ResourcesImpl;
 import android.content.res.ResourcesKey;
+import android.content.res.loader.ResourceLoader;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.IBinder;
 import android.os.Process;
@@ -45,6 +46,7 @@
 import android.view.Display;
 import android.view.DisplayAdjustments;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
@@ -53,7 +55,11 @@
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.function.Predicate;
@@ -92,6 +98,52 @@
             new ArrayMap<>();
 
     /**
+     * A list of {@link Resources} that contain unique {@link ResourcesImpl}s.
+     *
+     * These are isolated so that {@link ResourceLoader}s can be added and removed without
+     * affecting other instances.
+     *
+     * When a reference is added here, it is guaranteed that the {@link ResourcesImpl}
+     * it contains is unique to itself and will never be set to a shared reference.
+     */
+    @GuardedBy("this")
+    private List<ResourcesWithLoaders> mResourcesWithLoaders = Collections.emptyList();
+
+    private static class ResourcesWithLoaders {
+
+        private WeakReference<Resources> mResources;
+        private ResourcesKey mResourcesKey;
+
+        @Nullable
+        private WeakReference<IBinder> mActivityToken;
+
+        ResourcesWithLoaders(Resources resources, ResourcesKey resourcesKey,
+                IBinder activityToken) {
+            this.mResources = new WeakReference<>(resources);
+            this.mResourcesKey = resourcesKey;
+            this.mActivityToken = new WeakReference<>(activityToken);
+        }
+
+        @Nullable
+        Resources resources() {
+            return mResources.get();
+        }
+
+        @Nullable
+        IBinder activityToken() {
+            return mActivityToken == null ? null : mActivityToken.get();
+        }
+
+        ResourcesKey resourcesKey() {
+            return mResourcesKey;
+        }
+
+        void updateKey(ResourcesKey newKey) {
+            mResourcesKey = newKey;
+        }
+    }
+
+    /**
      * A list of Resource references that can be reused.
      */
     @UnsupportedAppUsage
@@ -182,15 +234,36 @@
     public void invalidatePath(String path) {
         synchronized (this) {
             int count = 0;
-            for (int i = 0; i < mResourceImpls.size();) {
+
+            for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
                 final ResourcesKey key = mResourceImpls.keyAt(i);
                 if (key.isPathReferenced(path)) {
-                    cleanupResourceImpl(key);
+                    ResourcesImpl impl = mResourceImpls.removeAt(i).get();
+                    if (impl != null) {
+                        impl.flushLayoutCache();
+                    }
                     count++;
-                } else {
-                    i++;
                 }
             }
+
+            for (int i = mResourcesWithLoaders.size() - 1; i >= 0; i--) {
+                ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(i);
+                Resources resources = resourcesWithLoaders.resources();
+                if (resources == null) {
+                    continue;
+                }
+
+                final ResourcesKey key = resourcesWithLoaders.resourcesKey();
+                if (key.isPathReferenced(path)) {
+                    mResourcesWithLoaders.remove(i);
+                    ResourcesImpl impl = resources.getImpl();
+                    if (impl != null) {
+                        impl.flushLayoutCache();
+                    }
+                    count++;
+                }
+            }
+
             Log.i(TAG, "Invalidated " + count + " asset managers that referenced " + path);
 
             for (int i = mCachedApkAssets.size() - 1; i >= 0; i--) {
@@ -317,15 +390,6 @@
         }
     }
 
-    private void cleanupResourceImpl(ResourcesKey removedKey) {
-        // Remove resource key to resource impl mapping and flush cache
-        final ResourcesImpl res = mResourceImpls.remove(removedKey).get();
-
-        if (res != null) {
-            res.flushLayoutCache();
-        }
-    }
-
     private static String overlayPathToIdmapPath(String path) {
         return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
     }
@@ -499,6 +563,16 @@
 
             pw.print("resource impls: ");
             pw.println(countLiveReferences(mResourceImpls.values()));
+
+            int resourcesWithLoadersCount = 0;
+            for (int index = 0; index < mResourcesWithLoaders.size(); index++) {
+                if (mResourcesWithLoaders.get(index).resources() != null) {
+                    resourcesWithLoadersCount++;
+                }
+            }
+
+            pw.print("resources with loaders: ");
+            pw.println(resourcesWithLoadersCount);
         }
     }
 
@@ -579,11 +653,24 @@
      */
     private @Nullable ResourcesKey findKeyForResourceImplLocked(
             @NonNull ResourcesImpl resourceImpl) {
-        final int refCount = mResourceImpls.size();
+        int size = mResourcesWithLoaders.size();
+        for (int index = 0; index < size; index++) {
+            ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+            Resources resources = resourcesWithLoaders.resources();
+            if (resources == null) {
+                continue;
+            }
+
+            if (resourceImpl == resources.getImpl()) {
+                return resourcesWithLoaders.resourcesKey();
+            }
+        }
+
+        int refCount = mResourceImpls.size();
         for (int i = 0; i < refCount; i++) {
             WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
             ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
-            if (impl != null && resourceImpl == impl) {
+            if (resourceImpl == impl) {
                 return mResourceImpls.keyAt(i);
             }
         }
@@ -625,31 +712,55 @@
         return activityResources;
     }
 
-    /**
-     * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
-     * or the class loader is different.
-     */
-    private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
+    @Nullable
+    private Resources findResourcesForActivityLocked(@NonNull IBinder targetActivityToken,
+            @NonNull ResourcesKey targetKey, @NonNull ClassLoader targetClassLoader) {
+        int size = mResourcesWithLoaders.size();
+        for (int index = 0; index < size; index++) {
+            ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+            Resources resources = resourcesWithLoaders.resources();
+            if (resources == null) {
+                continue;
+            }
+
+            IBinder activityToken = resourcesWithLoaders.activityToken();
+            ResourcesKey key = resourcesWithLoaders.resourcesKey();
+
+            ClassLoader classLoader = resources.getClassLoader();
+
+            if (Objects.equals(activityToken, targetActivityToken)
+                    && Objects.equals(key, targetKey)
+                    && Objects.equals(classLoader, targetClassLoader)) {
+                return resources;
+            }
+        }
+
+        ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+                targetActivityToken);
+
+        size = activityResources.activityResources.size();
+        for (int index = 0; index < size; index++) {
+            WeakReference<Resources> ref = activityResources.activityResources.get(index);
+            Resources resources = ref.get();
+            ResourcesKey key = resources == null ? null : findKeyForResourceImplLocked(
+                    resources.getImpl());
+
+            if (key != null
+                    && Objects.equals(resources.getClassLoader(), targetClassLoader)
+                    && Objects.equals(key, targetKey)) {
+                return resources;
+            }
+        }
+
+        return null;
+    }
+
+    private @NonNull Resources createResourcesForActivityLocked(@NonNull IBinder activityToken,
             @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
             @NonNull CompatibilityInfo compatInfo) {
         final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
                 activityToken);
 
-        final int refCount = activityResources.activityResources.size();
-        for (int i = 0; i < refCount; i++) {
-            WeakReference<Resources> weakResourceRef = activityResources.activityResources.get(i);
-            Resources resources = weakResourceRef.get();
-
-            if (resources != null
-                    && Objects.equals(resources.getClassLoader(), classLoader)
-                    && resources.getImpl() == impl) {
-                if (DEBUG) {
-                    Slog.d(TAG, "- using existing ref=" + resources);
-                }
-                return resources;
-            }
-        }
-
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
@@ -661,28 +772,8 @@
         return resources;
     }
 
-    /**
-     * Gets an existing Resources object if the class loader and ResourcesImpl are the same,
-     * otherwise creates a new Resources object.
-     */
-    private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
+    private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader,
             @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
-        // Find an existing Resources that has this ResourcesImpl set.
-        final int refCount = mResourceReferences.size();
-        for (int i = 0; i < refCount; i++) {
-            WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
-            Resources resources = weakResourceRef.get();
-            if (resources != null &&
-                    Objects.equals(resources.getClassLoader(), classLoader) &&
-                    resources.getImpl() == impl) {
-                if (DEBUG) {
-                    Slog.d(TAG, "- using existing ref=" + resources);
-                }
-                return resources;
-            }
-        }
-
-        // Create a new Resources reference and use the existing ResourcesImpl object.
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
@@ -750,16 +841,70 @@
             updateResourcesForActivity(activityToken, overrideConfig, displayId,
                     false /* movedToDifferentDisplay */);
 
+            cleanupReferences(activityToken);
+            rebaseKeyForActivity(activityToken, key);
+
+            synchronized (this) {
+                Resources resources = findResourcesForActivityLocked(activityToken, key,
+                        classLoader);
+                if (resources != null) {
+                    return resources;
+                }
+            }
+
             // Now request an actual Resources object.
-            return getOrCreateResources(activityToken, key, classLoader);
+            return createResources(activityToken, key, classLoader);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
         }
     }
 
     /**
-     * Gets an existing Resources object set with a ResourcesImpl object matching the given key,
-     * or creates one if it doesn't exist.
+     * Rebases a key's override config on top of the Activity's base override.
+     */
+    private void rebaseKeyForActivity(IBinder activityToken, ResourcesKey key) {
+        final ActivityResources activityResources =
+                getOrCreateActivityResourcesStructLocked(activityToken);
+
+        // Clean up any dead references so they don't pile up.
+        ArrayUtils.unstableRemoveIf(activityResources.activityResources,
+                sEmptyReferencePredicate);
+
+        // Rebase the key's override config on top of the Activity's base override.
+        if (key.hasOverrideConfiguration()
+                && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
+            final Configuration temp = new Configuration(activityResources.overrideConfig);
+            temp.updateFrom(key.mOverrideConfiguration);
+            key.mOverrideConfiguration.setTo(temp);
+        }
+    }
+
+    /**
+     * Check WeakReferences and remove any dead references so they don't pile up.
+     * @param activityToken optional token to clean up Activity resources
+     */
+    private void cleanupReferences(IBinder activityToken) {
+        if (activityToken != null) {
+            ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
+            if (activityResources != null) {
+                ArrayUtils.unstableRemoveIf(activityResources.activityResources,
+                        sEmptyReferencePredicate);
+            }
+        } else {
+            ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
+        }
+
+        for (int index = mResourcesWithLoaders.size() - 1; index >= 0; index--) {
+            ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+            Resources resources = resourcesWithLoaders.resources();
+            if (resources == null) {
+                mResourcesWithLoaders.remove(index);
+            }
+        }
+    }
+
+    /**
+     * Creates a Resources object set with a ResourcesImpl object matching the given key.
      *
      * @param activityToken The Activity this Resources object should be associated with.
      * @param key The key describing the parameters of the ResourcesImpl object.
@@ -769,7 +914,7 @@
      *         {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
      *         is called.
      */
-    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
+    private @Nullable Resources createResources(@Nullable IBinder activityToken,
             @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
         synchronized (this) {
             if (DEBUG) {
@@ -778,66 +923,17 @@
                 Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
             }
 
-            if (activityToken != null) {
-                final ActivityResources activityResources =
-                        getOrCreateActivityResourcesStructLocked(activityToken);
-
-                // Clean up any dead references so they don't pile up.
-                ArrayUtils.unstableRemoveIf(activityResources.activityResources,
-                        sEmptyReferencePredicate);
-
-                // Rebase the key's override config on top of the Activity's base override.
-                if (key.hasOverrideConfiguration()
-                        && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
-                    final Configuration temp = new Configuration(activityResources.overrideConfig);
-                    temp.updateFrom(key.mOverrideConfiguration);
-                    key.mOverrideConfiguration.setTo(temp);
-                }
-
-                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
-                if (resourcesImpl != null) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
-                    }
-                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
-                            resourcesImpl, key.mCompatInfo);
-                }
-
-                // We will create the ResourcesImpl object outside of holding this lock.
-
-            } else {
-                // Clean up any dead references so they don't pile up.
-                ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
-
-                // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
-                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
-                if (resourcesImpl != null) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
-                    }
-                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
-                }
-
-                // We will create the ResourcesImpl object outside of holding this lock.
-            }
-
-            // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
-            ResourcesImpl resourcesImpl = createResourcesImpl(key);
+            ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
             if (resourcesImpl == null) {
                 return null;
             }
 
-            // Add this ResourcesImpl to the cache.
-            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
-
-            final Resources resources;
             if (activityToken != null) {
-                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
+                return createResourcesForActivityLocked(activityToken, classLoader,
                         resourcesImpl, key.mCompatInfo);
             } else {
-                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
+                return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
             }
-            return resources;
         }
     }
 
@@ -868,7 +964,8 @@
      * {@link ClassLoader#getSystemClassLoader()} is used.
      * @return a Resources object from which to access resources.
      */
-    public @Nullable Resources getResources(@Nullable IBinder activityToken,
+    public @Nullable Resources getResources(
+            @Nullable IBinder activityToken,
             @Nullable String resDir,
             @Nullable String[] splitResDirs,
             @Nullable String[] overlayDirs,
@@ -888,7 +985,14 @@
                     overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                     compatInfo);
             classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
-            return getOrCreateResources(activityToken, key, classLoader);
+
+            cleanupReferences(activityToken);
+
+            if (activityToken != null) {
+                rebaseKeyForActivity(activityToken, key);
+            }
+
+            return createResources(activityToken, key, classLoader);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
         }
@@ -944,67 +1048,40 @@
                             here);
                 }
 
-                final boolean activityHasOverrideConfig =
-                        !activityResources.overrideConfig.equals(Configuration.EMPTY);
 
                 // Rebase each Resources associated with this Activity.
                 final int refCount = activityResources.activityResources.size();
                 for (int i = 0; i < refCount; i++) {
                     WeakReference<Resources> weakResRef = activityResources.activityResources.get(
                             i);
+
                     Resources resources = weakResRef.get();
                     if (resources == null) {
                         continue;
                     }
 
-                    // Extract the ResourcesKey that was last used to create the Resources for this
-                    // activity.
-                    final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
-                    if (oldKey == null) {
-                        Slog.e(TAG, "can't find ResourcesKey for resources impl="
-                                + resources.getImpl());
+                    ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig,
+                            overrideConfig, displayId);
+                    updateActivityResources(resources, newKey, false);
+                }
+
+                // Also find loaders that are associated with an Activity
+                final int loaderCount = mResourcesWithLoaders.size();
+                for (int index = loaderCount - 1; index >= 0; index--) {
+                    ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(
+                            index);
+                    Resources resources = resourcesWithLoaders.resources();
+                    if (resources == null
+                            || resourcesWithLoaders.activityToken() != activityToken) {
                         continue;
                     }
 
-                    // Build the new override configuration for this ResourcesKey.
-                    final Configuration rebasedOverrideConfig = new Configuration();
-                    if (overrideConfig != null) {
-                        rebasedOverrideConfig.setTo(overrideConfig);
-                    }
+                    ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig,
+                            overrideConfig, displayId);
 
-                    if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) {
-                        // Generate a delta between the old base Activity override configuration and
-                        // the actual final override configuration that was used to figure out the
-                        // real delta this Resources object wanted.
-                        Configuration overrideOverrideConfig = Configuration.generateDelta(
-                                oldConfig, oldKey.mOverrideConfiguration);
-                        rebasedOverrideConfig.updateFrom(overrideOverrideConfig);
-                    }
+                    updateActivityResources(resources, newKey, true);
 
-                    // Create the new ResourcesKey with the rebased override config.
-                    final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir,
-                            oldKey.mSplitResDirs,
-                            oldKey.mOverlayDirs, oldKey.mLibDirs, displayId,
-                            rebasedOverrideConfig, oldKey.mCompatInfo);
-
-                    if (DEBUG) {
-                        Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey
-                                + " to newKey=" + newKey + ", displayId=" + displayId);
-                    }
-
-                    ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey);
-                    if (resourcesImpl == null) {
-                        resourcesImpl = createResourcesImpl(newKey);
-                        if (resourcesImpl != null) {
-                            mResourceImpls.put(newKey, new WeakReference<>(resourcesImpl));
-                        }
-                    }
-
-                    if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
-                        // Set the ResourcesImpl, updating it for all users of this Resources
-                        // object.
-                        resources.setImpl(resourcesImpl);
-                    }
+                    resourcesWithLoaders.updateKey(newKey);
                 }
             }
         } finally {
@@ -1012,6 +1089,70 @@
         }
     }
 
+    /**
+     * Rebases an updated override config over any old override config and returns the new one
+     * that an Activity's Resources should be set to.
+     */
+    private ResourcesKey rebaseActivityOverrideConfig(Resources resources,
+            Configuration oldOverrideConfig, @Nullable Configuration newOverrideConfig,
+            int displayId) {
+        // Extract the ResourcesKey that was last used to create the Resources for this
+        // activity.
+        final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
+        if (oldKey == null) {
+            Slog.e(TAG, "can't find ResourcesKey for resources impl="
+                    + resources.getImpl());
+            return null;
+        }
+
+        // Build the new override configuration for this ResourcesKey.
+        final Configuration rebasedOverrideConfig = new Configuration();
+        if (newOverrideConfig != null) {
+            rebasedOverrideConfig.setTo(newOverrideConfig);
+        }
+
+        final boolean hadOverrideConfig = !oldOverrideConfig.equals(Configuration.EMPTY);
+        if (hadOverrideConfig && oldKey.hasOverrideConfiguration()) {
+            // Generate a delta between the old base Activity override configuration and
+            // the actual final override configuration that was used to figure out the
+            // real delta this Resources object wanted.
+            Configuration overrideOverrideConfig = Configuration.generateDelta(
+                    oldOverrideConfig, oldKey.mOverrideConfiguration);
+            rebasedOverrideConfig.updateFrom(overrideOverrideConfig);
+        }
+
+        // Create the new ResourcesKey with the rebased override config.
+        final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir,
+                oldKey.mSplitResDirs,
+                oldKey.mOverlayDirs, oldKey.mLibDirs, displayId,
+                rebasedOverrideConfig, oldKey.mCompatInfo);
+
+        if (DEBUG) {
+            Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey
+                    + " to newKey=" + newKey + ", displayId=" + displayId);
+        }
+
+        return newKey;
+    }
+
+    private void updateActivityResources(Resources resources, ResourcesKey newKey,
+            boolean hasLoader) {
+        final ResourcesImpl resourcesImpl;
+
+        if (hasLoader) {
+            // Loaders always get new Impls because they cannot be shared
+            resourcesImpl = createResourcesImpl(newKey);
+        } else {
+            resourcesImpl = findOrCreateResourcesImplForKeyLocked(newKey);
+        }
+
+        if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
+            // Set the ResourcesImpl, updating it for all users of this Resources
+            // object.
+            resources.setImpl(resourcesImpl);
+        }
+    }
+
     @TestApi
     public final boolean applyConfigurationToResources(@NonNull Configuration config,
             @Nullable CompatibilityInfo compat) {
@@ -1050,61 +1191,77 @@
             ApplicationPackageManager.configurationChanged();
             //Slog.i(TAG, "Configuration changed in " + currentPackageName());
 
-            Configuration tmpConfig = null;
+            Configuration tmpConfig = new Configuration();
 
             for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
                 ResourcesKey key = mResourceImpls.keyAt(i);
                 WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
                 ResourcesImpl r = weakImplRef != null ? weakImplRef.get() : null;
                 if (r != null) {
-                    if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "
-                            + r + " config to: " + config);
-                    int displayId = key.mDisplayId;
-                    boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
-                    DisplayMetrics dm = defaultDisplayMetrics;
-                    final boolean hasOverrideConfiguration = key.hasOverrideConfiguration();
-                    if (!isDefaultDisplay || hasOverrideConfiguration) {
-                        if (tmpConfig == null) {
-                            tmpConfig = new Configuration();
-                        }
-                        tmpConfig.setTo(config);
-
-                        // Get new DisplayMetrics based on the DisplayAdjustments given
-                        // to the ResourcesImpl. Update a copy if the CompatibilityInfo
-                        // changed, because the ResourcesImpl object will handle the
-                        // update internally.
-                        DisplayAdjustments daj = r.getDisplayAdjustments();
-                        if (compat != null) {
-                            daj = new DisplayAdjustments(daj);
-                            daj.setCompatibilityInfo(compat);
-                        }
-                        dm = getDisplayMetrics(displayId, daj);
-
-                        if (!isDefaultDisplay) {
-                            applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig);
-                        }
-
-                        if (hasOverrideConfiguration) {
-                            tmpConfig.updateFrom(key.mOverrideConfiguration);
-                        }
-                        r.updateConfiguration(tmpConfig, dm, compat);
-                    } else {
-                        r.updateConfiguration(config, dm, compat);
-                    }
-                    //Slog.i(TAG, "Updated app resources " + v.getKey()
-                    //        + " " + r + ": " + r.getConfiguration());
+                    applyConfigurationToResourcesLocked(config, compat, tmpConfig,
+                            defaultDisplayMetrics, key, r);
                 } else {
-                    //Slog.i(TAG, "Removing old resources " + v.getKey());
                     mResourceImpls.removeAt(i);
                 }
             }
 
+            for (int index = mResourcesWithLoaders.size() - 1; index >= 0; index--) {
+                ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+                Resources resources = resourcesWithLoaders.resources();
+                if (resources == null) {
+                    mResourcesWithLoaders.remove(index);
+                    continue;
+                }
+
+                applyConfigurationToResourcesLocked(config, compat, tmpConfig,
+                        defaultDisplayMetrics, resourcesWithLoaders.resourcesKey(),
+                        resources.getImpl());
+            }
+
             return changes != 0;
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
         }
     }
 
+    private void applyConfigurationToResourcesLocked(@NonNull Configuration config,
+            @Nullable CompatibilityInfo compat, Configuration tmpConfig,
+            DisplayMetrics defaultDisplayMetrics, ResourcesKey key, ResourcesImpl resourcesImpl) {
+        if (DEBUG || DEBUG_CONFIGURATION) {
+            Slog.v(TAG, "Changing resources "
+                    + resourcesImpl + " config to: " + config);
+        }
+        int displayId = key.mDisplayId;
+        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
+        DisplayMetrics dm = defaultDisplayMetrics;
+        final boolean hasOverrideConfiguration = key.hasOverrideConfiguration();
+        if (!isDefaultDisplay || hasOverrideConfiguration) {
+            tmpConfig.setTo(config);
+
+            // Get new DisplayMetrics based on the DisplayAdjustments given
+            // to the ResourcesImpl. Update a copy if the CompatibilityInfo
+            // changed, because the ResourcesImpl object will handle the
+            // update internally.
+            DisplayAdjustments daj = resourcesImpl.getDisplayAdjustments();
+            if (compat != null) {
+                daj = new DisplayAdjustments(daj);
+                daj.setCompatibilityInfo(compat);
+            }
+            dm = getDisplayMetrics(displayId, daj);
+
+            if (!isDefaultDisplay) {
+                applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig);
+            }
+
+            if (hasOverrideConfiguration) {
+                tmpConfig.updateFrom(key.mOverrideConfiguration);
+            }
+            resourcesImpl.updateConfiguration(tmpConfig, dm, compat);
+        } else {
+            resourcesImpl.updateConfiguration(config, dm, compat);
+        }
+    }
+
     /**
      * Appends the library asset path to any ResourcesImpl object that contains the main
      * assetPath.
@@ -1140,7 +1297,7 @@
                                 ArrayUtils.appendElement(String.class, newLibAssets, libAsset);
                     }
 
-                    if (newLibAssets != key.mLibDirs) {
+                    if (!Arrays.equals(newLibAssets, key.mLibDirs)) {
                         updatedResourceKeys.put(impl, new ResourcesKey(
                                 key.mResDir,
                                 key.mSplitResDirs,
@@ -1153,10 +1310,106 @@
                 }
             }
 
+            final int count = mResourcesWithLoaders.size();
+            for (int index = 0; index < count; index++) {
+                ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+                Resources resources = resourcesWithLoaders.resources();
+                if (resources == null) {
+                    continue;
+                }
+
+                ResourcesKey key = resourcesWithLoaders.resourcesKey();
+                if (Objects.equals(key.mResDir, assetPath)) {
+                    String[] newLibAssets = key.mLibDirs;
+                    for (String libAsset : libAssets) {
+                        newLibAssets =
+                                ArrayUtils.appendElement(String.class, newLibAssets, libAsset);
+                    }
+
+                    if (!Arrays.equals(newLibAssets, key.mLibDirs)) {
+                        updatedResourceKeys.put(resources.getImpl(),
+                                new ResourcesKey(
+                                        key.mResDir,
+                                        key.mSplitResDirs,
+                                        key.mOverlayDirs,
+                                        newLibAssets,
+                                        key.mDisplayId,
+                                        key.mOverrideConfiguration,
+                                        key.mCompatInfo));
+                    }
+                }
+            }
+
             redirectResourcesToNewImplLocked(updatedResourceKeys);
         }
     }
 
+    /**
+     * Mark a {@link Resources} as containing a {@link ResourceLoader}.
+     *
+     * This removes its {@link ResourcesImpl} from the shared pool and creates it a new one. It is
+     * then tracked separately, kept unique, and restored properly across {@link Activity}
+     * recreations.
+     */
+    public void registerForLoaders(Resources resources) {
+        synchronized (this) {
+            boolean found = false;
+            IBinder activityToken = null;
+
+            // Remove the Resources from the reference list as it's now tracked separately
+            int size = mResourceReferences.size();
+            for (int index = 0; index < size; index++) {
+                WeakReference<Resources> reference = mResourceReferences.get(index);
+                if (reference.get() == resources) {
+                    mResourceReferences.remove(index);
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) {
+                // Do the same removal for any Activity Resources
+                for (Map.Entry<IBinder, ActivityResources> entry :
+                        mActivityResourceReferences.entrySet()) {
+                    ArrayList<WeakReference<Resources>> activityResourcesList =
+                            entry.getValue().activityResources;
+                    final int resCount = activityResourcesList.size();
+                    for (int index = 0; index < resCount; index++) {
+                        WeakReference<Resources> reference = activityResourcesList.get(index);
+                        if (reference.get() == resources) {
+                            activityToken = entry.getKey();
+                            activityResourcesList.remove(index);
+                            found = true;
+                            break;
+                        }
+                    }
+
+                    if (found) {
+                        break;
+                    }
+                }
+            }
+
+            if (!found) {
+                throw new IllegalArgumentException("Resources " + resources
+                        + " registered for loaders but was not previously tracked by"
+                        + " ResourcesManager");
+            }
+
+            ResourcesKey key = findKeyForResourceImplLocked(resources.getImpl());
+            ResourcesImpl impl = createResourcesImpl(key);
+
+            if (mResourcesWithLoaders == Collections.EMPTY_LIST) {
+                mResourcesWithLoaders = Collections.synchronizedList(new ArrayList<>());
+            }
+
+            mResourcesWithLoaders.add(new ResourcesWithLoaders(resources, key, activityToken));
+
+            // Set the new Impl, which is now guaranteed to be unique per Resources object
+            resources.setImpl(impl);
+        }
+    }
+
     // TODO(adamlesinski): Make this accept more than just overlay directories.
     final void applyNewResourceDirsLocked(@NonNull final ApplicationInfo appInfo,
             @Nullable final String[] oldPaths) {
@@ -1201,6 +1454,32 @@
                 }
             }
 
+            final int count = mResourcesWithLoaders.size();
+            for (int index = 0; index < count; index++) {
+                ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+                Resources resources = resourcesWithLoaders.resources();
+                if (resources == null) {
+                    continue;
+                }
+
+                ResourcesKey key = resourcesWithLoaders.resourcesKey();
+
+                if (key.mResDir == null
+                        || key.mResDir.equals(baseCodePath)
+                        || ArrayUtils.contains(oldPaths, key.mResDir)) {
+                    updatedResourceKeys.put(resources.getImpl(),
+                            new ResourcesKey(
+                                    baseCodePath,
+                                    copiedSplitDirs,
+                                    copiedResourceDirs,
+                                    key.mLibDirs,
+                                    key.mDisplayId,
+                                    key.mOverrideConfiguration,
+                                    key.mCompatInfo
+                            ));
+                }
+            }
+
             redirectResourcesToNewImplLocked(updatedResourceKeys);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
@@ -1250,5 +1529,25 @@
                 }
             }
         }
+
+        // Update any references that need to be re-built with loaders. These are intentionally not
+        // part of either of the above lists.
+        final int loaderCount = mResourcesWithLoaders.size();
+        for (int index = loaderCount - 1; index >= 0; index--) {
+            ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index);
+            Resources resources = resourcesWithLoaders.resources();
+            if (resources == null) {
+                continue;
+            }
+
+            ResourcesKey newKey = updatedResourceKeys.get(resources.getImpl());
+            if (newKey == null) {
+                continue;
+            }
+
+            resourcesWithLoaders.updateKey(newKey);
+            resourcesWithLoaders.resources()
+                    .setImpl(createResourcesImpl(newKey));
+        }
     }
 }
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index a35ad56..de1d514 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -16,7 +16,10 @@
 package android.content.res;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UnsupportedAppUsage;
+import android.content.res.loader.ResourcesProvider;
+import android.text.TextUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
@@ -36,10 +39,14 @@
  */
 public final class ApkAssets {
     @GuardedBy("this") private final long mNativePtr;
+
+    @Nullable
     @GuardedBy("this") private final StringBlock mStringBlock;
 
     @GuardedBy("this") private boolean mOpen = true;
 
+    private final boolean mForLoader;
+
     /**
      * Creates a new ApkAssets instance from the given path on disk.
      *
@@ -48,7 +55,8 @@
      * @throws IOException if a disk I/O error or parsing error occurred.
      */
     public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
-        return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
+        return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/,
+                false /*arscOnly*/, false /*forLoader*/);
     }
 
     /**
@@ -61,7 +69,8 @@
      */
     public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system)
             throws IOException {
-        return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/);
+        return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/,
+                false /*arscOnly*/, false /*forLoader*/);
     }
 
     /**
@@ -76,7 +85,8 @@
      */
     public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system,
             boolean forceSharedLibrary) throws IOException {
-        return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/);
+        return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/,
+                false /*arscOnly*/, false /*forLoader*/);
     }
 
     /**
@@ -96,7 +106,8 @@
     public static @NonNull ApkAssets loadFromFd(@NonNull FileDescriptor fd,
             @NonNull String friendlyName, boolean system, boolean forceSharedLibrary)
             throws IOException {
-        return new ApkAssets(fd, friendlyName, system, forceSharedLibrary);
+        return new ApkAssets(fd, friendlyName, system, forceSharedLibrary, false /*arscOnly*/,
+                false /*forLoader*/);
     }
 
     /**
@@ -110,21 +121,90 @@
      */
     public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
             throws IOException {
-        return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
+        return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/,
+                false /*arscOnly*/, false /*forLoader*/);
     }
 
-    private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
+    /**
+     * Creates a new ApkAssets instance from the given path on disk for use with a
+     * {@link ResourcesProvider}.
+     *
+     * @param path The path to an APK on disk.
+     * @return a new instance of ApkAssets.
+     * @throws IOException if a disk I/O error or parsing error occurred.
+     */
+    public static @NonNull ApkAssets loadApkForLoader(@NonNull String path)
             throws IOException {
+        return new ApkAssets(path, false /*system*/, false /*forceSharedLibrary*/,
+                false /*overlay*/, false /*arscOnly*/, true /*forLoader*/);
+    }
+
+    /**
+     * Creates a new ApkAssets instance from the given file descriptor for use with a
+     * {@link ResourcesProvider}.
+     *
+     * Performs a dup of the underlying fd, so you must take care of still closing
+     * the FileDescriptor yourself (and can do that whenever you want).
+     *
+     * @param fd The FileDescriptor of an open, readable APK.
+     * @return a new instance of ApkAssets.
+     * @throws IOException if a disk I/O error or parsing error occurred.
+     */
+    @NonNull
+    public static ApkAssets loadApkForLoader(@NonNull FileDescriptor fd) throws IOException {
+        return new ApkAssets(fd, TextUtils.emptyIfNull(fd.toString()),
+                false /*system*/, false /*forceSharedLib*/, false /*arscOnly*/, true /*forLoader*/);
+    }
+
+    /**
+     * Creates a new ApkAssets instance from the given file descriptor representing an ARSC
+     * for use with a {@link ResourcesProvider}.
+     *
+     * Performs a dup of the underlying fd, so you must take care of still closing
+     * the FileDescriptor yourself (and can do that whenever you want).
+     *
+     * @param fd The FileDescriptor of an open, readable .arsc.
+     * @return a new instance of ApkAssets.
+     * @throws IOException if a disk I/O error or parsing error occurred.
+     */
+    public static @NonNull ApkAssets loadArscForLoader(@NonNull FileDescriptor fd)
+            throws IOException {
+        return new ApkAssets(fd, TextUtils.emptyIfNull(fd.toString()),
+                false /*system*/, false /*forceSharedLib*/, true /*arscOnly*/, true /*forLoader*/);
+    }
+
+    /**
+     * Generates an entirely empty ApkAssets. Needed because the ApkAssets instance and presence
+     * is required for a lot of APIs, and it's easier to have a non-null reference rather than
+     * tracking a separate identifier.
+     */
+    @NonNull
+    public static ApkAssets loadEmptyForLoader() {
+        return new ApkAssets(true);
+    }
+
+    private ApkAssets(boolean forLoader) {
+        mForLoader = forLoader;
+        mNativePtr = nativeLoadEmpty(forLoader);
+        mStringBlock = null;
+    }
+
+    private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay,
+            boolean arscOnly, boolean forLoader) throws IOException {
+        mForLoader = forLoader;
         Preconditions.checkNotNull(path, "path");
-        mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
+        mNativePtr = arscOnly ? nativeLoadArsc(path, forLoader)
+                : nativeLoad(path, system, forceSharedLib, overlay, forLoader);
         mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
     }
 
     private ApkAssets(@NonNull FileDescriptor fd, @NonNull String friendlyName, boolean system,
-            boolean forceSharedLib) throws IOException {
+            boolean forceSharedLib, boolean arscOnly, boolean forLoader) throws IOException {
+        mForLoader = forLoader;
         Preconditions.checkNotNull(fd, "fd");
         Preconditions.checkNotNull(friendlyName, "friendlyName");
-        mNativePtr = nativeLoadFromFd(fd, friendlyName, system, forceSharedLib);
+        mNativePtr = arscOnly ? nativeLoadArscFromFd(fd, friendlyName, forLoader)
+                : nativeLoadFromFd(fd, friendlyName, system, forceSharedLib, forLoader);
         mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
     }
 
@@ -136,11 +216,19 @@
     }
 
     CharSequence getStringFromPool(int idx) {
+        if (mStringBlock == null) {
+            return null;
+        }
+
         synchronized (this) {
             return mStringBlock.get(idx);
         }
     }
 
+    public boolean isForLoader() {
+        return mForLoader;
+    }
+
     /**
      * Retrieve a parser for a compiled XML file. This is associated with a single APK and
      * <em>NOT</em> a full AssetManager. This means that shared-library references will not be
@@ -192,18 +280,26 @@
         synchronized (this) {
             if (mOpen) {
                 mOpen = false;
-                mStringBlock.close();
+                if (mStringBlock != null) {
+                    mStringBlock.close();
+                }
                 nativeDestroy(mNativePtr);
             }
         }
     }
 
-    private static native long nativeLoad(
-            @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
+    private static native long nativeLoad(@NonNull String path, boolean system,
+            boolean forceSharedLib, boolean overlay, boolean forLoader)
             throws IOException;
     private static native long nativeLoadFromFd(@NonNull FileDescriptor fd,
-            @NonNull String friendlyName, boolean system, boolean forceSharedLib)
+            @NonNull String friendlyName, boolean system, boolean forceSharedLib,
+            boolean forLoader)
             throws IOException;
+    private static native long nativeLoadArsc(@NonNull String path, boolean forLoader)
+            throws IOException;
+    private static native long nativeLoadArscFromFd(@NonNull FileDescriptor fd,
+            @NonNull String friendlyName, boolean forLoader) throws IOException;
+    private static native long nativeLoadEmpty(boolean forLoader);
     private static native void nativeDestroy(long ptr);
     private static native @NonNull String nativeGetAssetPath(long ptr);
     private static native long nativeGetStringBlock(long ptr);
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 7d6dc97..23e7720 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -27,9 +27,13 @@
 import android.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration.NativeConfig;
+import android.content.res.loader.ResourceLoader;
+import android.content.res.loader.ResourceLoaderManager;
+import android.content.res.loader.ResourcesProvider;
 import android.os.ParcelFileDescriptor;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.util.TypedValue;
 
@@ -39,15 +43,19 @@
 import libcore.io.IoUtils;
 
 import java.io.BufferedReader;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.channels.FileLock;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 /**
@@ -110,6 +118,13 @@
     @GuardedBy("this") private int mNumRefs = 1;
     @GuardedBy("this") private HashMap<Long, RuntimeException> mRefStacks;
 
+    private ResourceLoaderManager mResourceLoaderManager;
+
+    /** @hide */
+    public void setResourceLoaderManager(ResourceLoaderManager resourceLoaderManager) {
+        mResourceLoaderManager = resourceLoaderManager;
+    }
+
     /**
      * A Builder class that helps create an AssetManager with only a single invocation of
      * {@link AssetManager#setApkAssets(ApkAssets[], boolean)}. Without using this builder,
@@ -507,7 +522,7 @@
                     outValue.changingConfigurations);
 
             if (outValue.type == TypedValue.TYPE_STRING) {
-                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
+                outValue.string = getPooledStringForCookie(cookie, outValue.data);
             }
             return true;
         }
@@ -554,7 +569,7 @@
                     outValue.changingConfigurations);
 
             if (outValue.type == TypedValue.TYPE_STRING) {
-                return mApkAssets[cookie - 1].getStringFromPool(outValue.data);
+                return getPooledStringForCookie(cookie, outValue.data);
             }
             return outValue.coerceToString();
         }
@@ -632,7 +647,7 @@
                 int cookie = rawInfoArray[i];
                 int index = rawInfoArray[i + 1];
                 retArray[j] = (index >= 0 && cookie > 0)
-                        ? mApkAssets[cookie - 1].getStringFromPool(index) : null;
+                        ? getPooledStringForCookie(cookie, index) : null;
             }
             return retArray;
         }
@@ -688,7 +703,7 @@
                     outValue.changingConfigurations);
 
             if (outValue.type == TypedValue.TYPE_STRING) {
-                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
+                outValue.string = getPooledStringForCookie(cookie, outValue.data);
             }
             return true;
         }
@@ -753,6 +768,7 @@
      *
      * @hide
      */
+    @TestApi
     public void setResourceResolutionLoggingEnabled(boolean enabled) {
         synchronized (this) {
             ensureValidLocked();
@@ -768,6 +784,7 @@
      *
      * @hide
      */
+    @TestApi
     public @Nullable String getLastResourceResolution() {
         synchronized (this) {
             ensureValidLocked();
@@ -814,6 +831,13 @@
         Preconditions.checkNotNull(fileName, "fileName");
         synchronized (this) {
             ensureOpenLocked();
+
+            String path = Paths.get("assets", fileName).toString();
+            InputStream inputStream = searchLoaders(0, path, accessMode);
+            if (inputStream != null) {
+                return inputStream;
+            }
+
             final long asset = nativeOpenAsset(mObject, fileName, accessMode);
             if (asset == 0) {
                 throw new FileNotFoundException("Asset file: " + fileName);
@@ -838,6 +862,13 @@
         Preconditions.checkNotNull(fileName, "fileName");
         synchronized (this) {
             ensureOpenLocked();
+
+            String path = Paths.get("assets", fileName).toString();
+            AssetFileDescriptor fileDescriptor = searchLoadersFd(0, path);
+            if (fileDescriptor != null) {
+                return fileDescriptor;
+            }
+
             final ParcelFileDescriptor pfd = nativeOpenAssetFd(mObject, fileName, mOffsets);
             if (pfd == null) {
                 throw new FileNotFoundException("Asset file: " + fileName);
@@ -931,6 +962,12 @@
         Preconditions.checkNotNull(fileName, "fileName");
         synchronized (this) {
             ensureOpenLocked();
+
+            InputStream inputStream = searchLoaders(cookie, fileName, accessMode);
+            if (inputStream != null) {
+                return inputStream;
+            }
+
             final long asset = nativeOpenNonAsset(mObject, cookie, fileName, accessMode);
             if (asset == 0) {
                 throw new FileNotFoundException("Asset absolute file: " + fileName);
@@ -970,6 +1007,12 @@
         Preconditions.checkNotNull(fileName, "fileName");
         synchronized (this) {
             ensureOpenLocked();
+
+            AssetFileDescriptor fileDescriptor = searchLoadersFd(cookie, fileName);
+            if (fileDescriptor != null) {
+                return fileDescriptor;
+            }
+
             final ParcelFileDescriptor pfd =
                     nativeOpenNonAssetFd(mObject, cookie, fileName, mOffsets);
             if (pfd == null) {
@@ -1031,7 +1074,16 @@
         Preconditions.checkNotNull(fileName, "fileName");
         synchronized (this) {
             ensureOpenLocked();
-            final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
+
+            final long xmlBlock;
+            AssetFileDescriptor fileDescriptor = searchLoadersFd(cookie, fileName);
+            if (fileDescriptor != null) {
+                xmlBlock = nativeOpenXmlAssetFd(mObject, cookie,
+                        fileDescriptor.getFileDescriptor());
+            } else {
+                xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
+            }
+
             if (xmlBlock == 0) {
                 throw new FileNotFoundException("Asset XML file: " + fileName);
             }
@@ -1041,6 +1093,85 @@
         }
     }
 
+    private InputStream searchLoaders(int cookie, @NonNull String fileName, int accessMode)
+            throws IOException {
+        if (mResourceLoaderManager == null) {
+            return null;
+        }
+
+        List<Pair<ResourceLoader, ResourcesProvider>> loaders =
+                mResourceLoaderManager.getInternalList();
+
+        // A cookie of 0 means no specific ApkAssets, so search everything
+        if (cookie == 0) {
+            for (int index = loaders.size() - 1; index >= 0; index--) {
+                Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index);
+                try {
+                    InputStream inputStream = pair.first.loadAsset(fileName, accessMode);
+                    if (inputStream != null) {
+                        return inputStream;
+                    }
+                } catch (IOException ignored) {
+                    // When searching, ignore read failures
+                }
+            }
+
+            return null;
+        }
+
+        ApkAssets apkAssets = mApkAssets[cookie - 1];
+        for (int index = loaders.size() - 1; index >= 0; index--) {
+            Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index);
+            if (pair.second.getApkAssets() == apkAssets) {
+                return pair.first.loadAsset(fileName, accessMode);
+            }
+        }
+
+        return null;
+    }
+
+    private AssetFileDescriptor searchLoadersFd(int cookie, @NonNull String fileName)
+            throws IOException {
+        if (mResourceLoaderManager == null) {
+            return null;
+        }
+
+        List<Pair<ResourceLoader, ResourcesProvider>> loaders =
+                mResourceLoaderManager.getInternalList();
+
+        // A cookie of 0 means no specific ApkAssets, so search everything
+        if (cookie == 0) {
+            for (int index = loaders.size() - 1; index >= 0; index--) {
+                Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index);
+                try {
+                    ParcelFileDescriptor fileDescriptor = pair.first.loadAssetFd(fileName);
+                    if (fileDescriptor != null) {
+                        return new AssetFileDescriptor(fileDescriptor, 0,
+                                AssetFileDescriptor.UNKNOWN_LENGTH);
+                    }
+                } catch (IOException ignored) {
+                    // When searching, ignore read failures
+                }
+            }
+
+            return null;
+        }
+
+        ApkAssets apkAssets = mApkAssets[cookie - 1];
+        for (int index = loaders.size() - 1; index >= 0; index--) {
+            Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index);
+            if (pair.second.getApkAssets() == apkAssets) {
+                ParcelFileDescriptor fileDescriptor = pair.first.loadAssetFd(fileName);
+                if (fileDescriptor != null) {
+                    return new AssetFileDescriptor(fileDescriptor, 0,
+                            AssetFileDescriptor.UNKNOWN_LENGTH);
+                }
+                return null;
+            }
+        }
+        return null;
+    }
+
     void xmlBlockGone(int id) {
         synchronized (this) {
             decRefsLocked(id);
@@ -1296,7 +1427,7 @@
      *
      * <p>On SDK 21 (Android 5.0: Lollipop) and above, Locale strings are valid
      * <a href="https://tools.ietf.org/html/bcp47">BCP-47</a> language tags and can be
-     * parsed using {@link java.util.Locale#forLanguageTag(String)}.
+     * parsed using {@link Locale#forLanguageTag(String)}.
      *
      * <p>On SDK 20 (Android 4.4W: Kitkat for watches) and below, locale strings
      * are of the form {@code ll_CC} where {@code ll} is a two letter language code,
@@ -1439,6 +1570,8 @@
     private static native @Nullable ParcelFileDescriptor nativeOpenNonAssetFd(long ptr, int cookie,
             @NonNull String fileName, @NonNull long[] outOffsets) throws IOException;
     private static native long nativeOpenXmlAsset(long ptr, int cookie, @NonNull String fileName);
+    private static native long nativeOpenXmlAssetFd(long ptr, int cookie,
+            @NonNull FileDescriptor fileDescriptor);
 
     // Primitive resource native methods.
     private static native int nativeGetResourceValue(long ptr, @AnyRes int resId, short density,
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index d7e4e14..2698c2d 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -30,6 +30,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.FontRes;
 import android.annotation.FractionRes;
+import android.annotation.IntRange;
 import android.annotation.IntegerRes;
 import android.annotation.LayoutRes;
 import android.annotation.NonNull;
@@ -41,8 +42,12 @@
 import android.annotation.StyleableRes;
 import android.annotation.UnsupportedAppUsage;
 import android.annotation.XmlRes;
+import android.app.ResourcesManager;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityInfo.Config;
+import android.content.res.loader.ResourceLoader;
+import android.content.res.loader.ResourceLoaderManager;
+import android.content.res.loader.ResourcesProvider;
 import android.graphics.Movie;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
@@ -54,13 +59,16 @@
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.Pair;
 import android.util.Pools.SynchronizedPool;
 import android.util.TypedValue;
 import android.view.DisplayAdjustments;
 import android.view.ViewDebug;
 import android.view.ViewHierarchyEncoder;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 import com.android.internal.util.XmlUtils;
 
@@ -71,6 +79,8 @@
 import java.io.InputStream;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Class for accessing an application's resources.  This sits on top of the
@@ -133,6 +143,11 @@
     @UnsupportedAppUsage
     final ClassLoader mClassLoader;
 
+    private final Object mResourceLoaderLock = new Object();
+
+    @GuardedBy("mResourceLoaderLock")
+    private ResourceLoaderManager mResourceLoaderManager;
+
     /**
      * WeakReferences to Themes that were constructed from this Resources object.
      * We keep track of these in case our underlying implementation is changed, in which case
@@ -148,6 +163,8 @@
     private static final int MIN_THEME_REFS_FLUSH_SIZE = 32;
     private int mThemeRefsNextFlushSize = MIN_THEME_REFS_FLUSH_SIZE;
 
+    private int mBaseApkAssetsSize;
+
     /**
      * Returns the most appropriate default theme for the specified target SDK version.
      * <ul>
@@ -283,8 +300,15 @@
             return;
         }
 
+        mBaseApkAssetsSize = ArrayUtils.size(impl.getAssets().getApkAssets());
         mResourcesImpl = impl;
 
+        synchronized (mResourceLoaderLock) {
+            if (mResourceLoaderManager != null) {
+                mResourceLoaderManager.onImplUpdate(mResourcesImpl);
+            }
+        }
+
         // Create new ThemeImpls that are identical to the ones we have.
         synchronized (mThemeRefs) {
             final int count = mThemeRefs.size();
@@ -903,7 +927,7 @@
         try {
             final ResourcesImpl impl = mResourcesImpl;
             impl.getValueForDensity(id, density, value, true);
-            return impl.loadDrawable(this, value, id, density, theme);
+            return loadDrawable(value, id, density, theme);
         } finally {
             releaseTempTypedValue(value);
         }
@@ -913,6 +937,14 @@
     @UnsupportedAppUsage
     Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
             throws NotFoundException {
+        ResourceLoader loader = findLoader(value.assetCookie);
+        if (loader != null) {
+            Drawable drawable = loader.loadDrawable(value, id, density, theme);
+            if (drawable != null) {
+                return drawable;
+            }
+        }
+
         return mResourcesImpl.loadDrawable(this, value, id, density, theme);
     }
 
@@ -2280,7 +2312,7 @@
             final ResourcesImpl impl = mResourcesImpl;
             impl.getValue(id, value, true);
             if (value.type == TypedValue.TYPE_STRING) {
-                return impl.loadXmlResourceParser(value.string.toString(), id,
+                return loadXmlResourceParser(value.string.toString(), id,
                         value.assetCookie, type);
             }
             throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
@@ -2304,6 +2336,14 @@
     @UnsupportedAppUsage
     XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie,
                                             String type) throws NotFoundException {
+        ResourceLoader loader = findLoader(assetCookie);
+        if (loader != null) {
+            XmlResourceParser xml = loader.loadXmlResourceParser(file, id);
+            if (xml != null) {
+                return xml;
+            }
+        }
+
         return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type);
     }
 
@@ -2329,4 +2369,137 @@
         }
         return theme.obtainStyledAttributes(set, attrs, 0, 0);
     }
+
+    private ResourceLoader findLoader(int assetCookie) {
+        ApkAssets[] apkAssetsArray = mResourcesImpl.getAssets().getApkAssets();
+        int apkAssetsIndex = assetCookie - 1;
+        if (apkAssetsIndex < apkAssetsArray.length && apkAssetsIndex >= 0) {
+            ApkAssets apkAssets = apkAssetsArray[apkAssetsIndex];
+            if (apkAssets.isForLoader()) {
+                List<Pair<ResourceLoader, ResourcesProvider>> loaders;
+                // Since we don't lock the entire resolution path anyways,
+                // only lock here instead of entire method. The list is copied
+                // and effectively a snapshot is used.
+                synchronized (mResourceLoaderLock) {
+                    loaders = mResourceLoaderManager.getInternalList();
+                }
+
+                if (!ArrayUtils.isEmpty(loaders)) {
+                    int size = loaders.size();
+                    for (int index = 0; index < size; index++) {
+                        Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index);
+                        if (pair.second.getApkAssets() == apkAssets) {
+                            return pair.first;
+                        }
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @return copied list of loaders and providers previously added
+     */
+    @NonNull
+    public List<Pair<ResourceLoader, ResourcesProvider>> getLoaders() {
+        synchronized (mResourceLoaderLock) {
+            return mResourceLoaderManager == null
+                    ? Collections.emptyList()
+                    : mResourceLoaderManager.getLoaders();
+        }
+    }
+
+    /**
+     * Add a custom {@link ResourceLoader} which is added to the paths searched by
+     * {@link AssetManager} when resolving a resource.
+     *
+     * Resources are resolved as if the loader was a resource overlay, meaning the latest
+     * in the list, of equal or better config, is returned.
+     *
+     * {@link ResourcesProvider}s passed in here are not managed and a reference should be held
+     * to remove, re-use, or close them when necessary.
+     *
+     * @param resourceLoader an interface used to resolve file paths for drawables/XML files;
+     *                       a reference should be kept to remove the loader if necessary
+     * @param resourcesProvider an .apk or .arsc file representation
+     * @param index where to add the loader in the list
+     * @throws IllegalArgumentException if the resourceLoader is already added
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public void addLoader(@NonNull ResourceLoader resourceLoader,
+            @NonNull ResourcesProvider resourcesProvider, @IntRange(from = 0) int index) {
+        synchronized (mResourceLoaderLock) {
+            if (mResourceLoaderManager == null) {
+                ResourcesManager.getInstance().registerForLoaders(this);
+                mResourceLoaderManager = new ResourceLoaderManager(mResourcesImpl);
+            }
+
+            mResourceLoaderManager.addLoader(resourceLoader, resourcesProvider, index);
+        }
+    }
+
+    /**
+     * @see #addLoader(ResourceLoader, ResourcesProvider, int).
+     *
+     * Adds to the end of the list.
+     *
+     * @return index the loader was added at
+     */
+    public int addLoader(@NonNull ResourceLoader resourceLoader,
+            @NonNull ResourcesProvider resourcesProvider) {
+        synchronized (mResourceLoaderLock) {
+            int index = getLoaders().size();
+            addLoader(resourceLoader, resourcesProvider, index);
+            return index;
+        }
+    }
+
+    /**
+     * Remove a loader previously added by
+     * {@link #addLoader(ResourceLoader, ResourcesProvider, int)}
+     *
+     * The caller maintains responsibility for holding a reference to the matching
+     * {@link ResourcesProvider} and closing it after this method has been called.
+     *
+     * @param resourceLoader the same reference passed into [addLoader
+     * @return the index the loader was at in the list, or -1 if the loader was not found
+     */
+    public int removeLoader(@NonNull ResourceLoader resourceLoader) {
+        synchronized (mResourceLoaderLock) {
+            if (mResourceLoaderManager == null) {
+                return -1;
+            }
+
+            return mResourceLoaderManager.removeLoader(resourceLoader);
+        }
+    }
+
+    /**
+     * Swap the current set of loaders. Preferred to multiple remove/add calls as this doesn't
+     * update the resource data structures after each modification.
+     *
+     * Set to null or an empty list to clear the set of loaders.
+     *
+     * The caller maintains responsibility for holding references to the added
+     * {@link ResourcesProvider}s and closing them after this method has been called.
+     *
+     * @param resourceLoadersAndProviders a list of pairs to add
+     */
+    public void setLoaders(
+            @Nullable List<Pair<ResourceLoader, ResourcesProvider>> resourceLoadersAndProviders) {
+        synchronized (mResourceLoaderLock) {
+            if (mResourceLoaderManager == null) {
+                if (ArrayUtils.isEmpty(resourceLoadersAndProviders)) {
+                    return;
+                }
+
+                ResourcesManager.getInstance().registerForLoaders(this);
+                mResourceLoaderManager = new ResourceLoaderManager(mResourcesImpl);
+            }
+
+            mResourceLoaderManager.setLoaders(resourceLoadersAndProviders);
+        }
+    }
 }
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index b72544c..84489cf 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -57,6 +57,8 @@
 
 import com.android.internal.util.GrowingArrayUtils;
 
+import libcore.io.IoUtils;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -376,7 +378,7 @@
         Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration");
         try {
             synchronized (mAccessLock) {
-                if (false) {
+                if (DEBUG_CONFIG) {
                     Slog.i(TAG, "**** Updating config of " + this + ": old config is "
                             + mConfiguration + " old compat is "
                             + mDisplayAdjustments.getCompatibilityInfo());
@@ -572,6 +574,20 @@
         }
     }
 
+    /**
+     * Wipe all caches that might be read and return an outdated object when resolving a resource.
+     */
+    public void clearAllCaches() {
+        synchronized (mAccessLock) {
+            mDrawableCache.clear();
+            mColorDrawableCache.clear();
+            mComplexColorCache.clear();
+            mAnimatorCache.clear();
+            mStateListAnimatorCache.clear();
+            flushLayoutCache();
+        }
+    }
+
     @Nullable
     Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
             int density, @Nullable Resources.Theme theme)
@@ -802,6 +818,27 @@
     }
 
     /**
+     * Loads a Drawable from an encoded image stream, or null.
+     *
+     * This call will handle closing the {@link InputStream}.
+     */
+    @Nullable
+    private Drawable decodeImageDrawable(@NonNull InputStream inputStream,
+            @NonNull Resources wrapper, @NonNull TypedValue value) {
+        ImageDecoder.Source src = ImageDecoder.createSource(wrapper, inputStream, value.density);
+        try {
+            return ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
+                    decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE));
+        } catch (IOException ignored) {
+            // This is okay. This may be something that ImageDecoder does not
+            // support, like SVG.
+            return null;
+        } finally {
+            IoUtils.closeQuietly(inputStream);
+        }
+    }
+
+    /**
      * Loads a drawable from XML or resources stream.
      *
      * @return Drawable, or null if Drawable cannot be decoded.
@@ -865,8 +902,12 @@
                 } else {
                     final InputStream is = mAssets.openNonAsset(
                             value.assetCookie, file, AssetManager.ACCESS_STREAMING);
-                    AssetInputStream ais = (AssetInputStream) is;
-                    dr = decodeImageDrawable(ais, wrapper, value);
+                    if (is instanceof AssetInputStream) {
+                        AssetInputStream ais = (AssetInputStream) is;
+                        dr = decodeImageDrawable(ais, wrapper, value);
+                    } else {
+                        dr = decodeImageDrawable(is, wrapper, value);
+                    }
                 }
             } finally {
                 stack.pop();
diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java
index 2ae1932..d43bd36 100644
--- a/core/java/android/content/res/StringBlock.java
+++ b/core/java/android/content/res/StringBlock.java
@@ -47,6 +47,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.io.Closeable;
 import java.util.Arrays;
 
 /**
@@ -54,7 +55,7 @@
  *
  * {@hide}
  */
-final class StringBlock {
+public final class StringBlock implements Closeable {
     private static final String TAG = "AssetManager";
     private static final boolean localLOGV = false;
 
@@ -175,6 +176,7 @@
         }
     }
 
+    @Override
     public void close() {
         synchronized (this) {
             if (mOpen) {
@@ -517,7 +519,7 @@
      *  of this newly creating StringBlock.
      */
     @UnsupportedAppUsage
-    StringBlock(long obj, boolean useSparse) {
+    public StringBlock(long obj, boolean useSparse) {
         mNative = obj;
         mUseSparse = useSparse;
         mOwnsNative = false;
diff --git a/core/java/android/content/res/ThemedResourceCache.java b/core/java/android/content/res/ThemedResourceCache.java
index 06cafdb..968ab40 100644
--- a/core/java/android/content/res/ThemedResourceCache.java
+++ b/core/java/android/content/res/ThemedResourceCache.java
@@ -22,8 +22,8 @@
 import android.content.pm.ActivityInfo.Config;
 import android.content.res.Resources.Theme;
 import android.content.res.Resources.ThemeKey;
-import android.util.LongSparseArray;
 import android.util.ArrayMap;
+import android.util.LongSparseArray;
 
 import java.lang.ref.WeakReference;
 
@@ -234,4 +234,18 @@
         return entry == null || (configChanges != 0
                 && shouldInvalidateEntry(entry, configChanges));
     }
+
+    public synchronized void clear() {
+        if (mThemedEntries != null) {
+            mThemedEntries.clear();
+        }
+
+        if (mUnthemedEntries != null) {
+            mUnthemedEntries.clear();
+        }
+
+        if (mNullThemedEntries != null) {
+            mNullThemedEntries.clear();
+        }
+    }
 }
diff --git a/core/java/android/content/res/loader/DirectoryResourceLoader.java b/core/java/android/content/res/loader/DirectoryResourceLoader.java
new file mode 100644
index 0000000..7d90e72
--- /dev/null
+++ b/core/java/android/content/res/loader/DirectoryResourceLoader.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 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.loader;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link ResourceLoader} that searches a directory for assets.
+ *
+ * Assumes that resource paths are resolvable child paths of the directory passed in.
+ */
+public class DirectoryResourceLoader implements ResourceLoader {
+
+    @NonNull
+    private final File mDirectory;
+
+    public DirectoryResourceLoader(@NonNull File directory) {
+        this.mDirectory = directory;
+    }
+
+    @Nullable
+    @Override
+    public InputStream loadAsset(@NonNull String path, int accessMode) throws IOException {
+        File file = findFile(path);
+        if (file == null || !file.exists()) {
+            return null;
+        }
+        return new FileInputStream(file);
+    }
+
+    @Nullable
+    @Override
+    public ParcelFileDescriptor loadAssetFd(@NonNull String path) throws IOException {
+        File file = findFile(path);
+        if (file == null || !file.exists()) {
+            return null;
+        }
+        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+
+    /**
+     * Find the file for the given path encoded into the resource table.
+     */
+    @Nullable
+    public File findFile(@NonNull String path) {
+        return mDirectory.toPath().resolve(path).toFile();
+    }
+
+    @NonNull
+    public File getDirectory() {
+        return mDirectory;
+    }
+}
diff --git a/core/java/android/content/res/loader/ResourceLoader.java b/core/java/android/content/res/loader/ResourceLoader.java
new file mode 100644
index 0000000..af32aa2
--- /dev/null
+++ b/core/java/android/content/res/loader/ResourceLoader.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 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.loader;
+
+import android.annotation.AnyRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.os.ParcelFileDescriptor;
+import android.util.TypedValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Exposes methods for overriding file-based resource loading from a {@link Resources}.
+ *
+ * To be used with {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)} and related
+ * methods to override resource loading.
+ *
+ * Note that this class doesn't actually contain any resource data. Non-file-based resources are
+ * loaded directly from the {@link ResourcesProvider}'s .arsc representation.
+ *
+ * An instance's methods will only be called if its corresponding {@link ResourcesProvider}'s
+ * resources table contains an entry for the resource ID being resolved,
+ * with the exception of the non-cookie variants of {@link AssetManager}'s openAsset and
+ * openNonAsset.
+ *
+ * Those methods search backwards through all {@link ResourceLoader}s and then any paths provided
+ * by the application or system.
+ *
+ * Otherwise, an ARSC that defines R.drawable.some_id must be provided if a {@link ResourceLoader}
+ * wants to point R.drawable.some_id to a different file on disk.
+ */
+public interface ResourceLoader {
+
+    /**
+     * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to
+     * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return a
+     * {@link Drawable} which should be returned by the parent
+     * {@link Resources#getDrawable(int, Resources.Theme)}.
+     *
+     * @param value   the resolved {@link TypedValue} before it has been converted to a Drawable
+     *                object
+     * @param id      the R.drawable ID this resolution is for
+     * @param density the requested density
+     * @param theme   the {@link Resources.Theme} resolved under
+     * @return null if resolution should try to find an entry inside the {@link ResourcesProvider},
+     * including calling through to {@link #loadAsset(String, int)} or {@link #loadAssetFd(String)}
+     */
+    @Nullable
+    default Drawable loadDrawable(@NonNull TypedValue value, int id, int density,
+            @Nullable Resources.Theme theme) {
+        return null;
+    }
+
+    /**
+     * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to
+     * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return an
+     * {@link XmlResourceParser} which should be returned by the parent
+     * {@link Resources#getDrawable(int, Resources.Theme)}.
+     *
+     * @param path the string that was found in the string pool
+     * @param id   the XML ID this resolution is for, can be R.anim, R.layout, or R.xml
+     * @return null if resolution should try to find an entry inside the {@link ResourcesProvider},
+     * including calling through to {@link #loadAssetFd(String)} (String, int)}
+     */
+    @Nullable
+    default XmlResourceParser loadXmlResourceParser(@NonNull String path, @AnyRes int id) {
+        return null;
+    }
+
+    /**
+     * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to
+     * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return an
+     * {@link InputStream} which should be returned when an asset is loaded by {@link AssetManager}.
+     * Assets will be loaded from a provider's root, with anything in its assets subpath prefixed
+     * with "assets/".
+     *
+     * @param path       the asset path to load
+     * @param accessMode {@link AssetManager} access mode; does not have to be respected
+     * @return null if resolution should try to find an entry inside the {@link ResourcesProvider}
+     */
+    @Nullable
+    default InputStream loadAsset(@NonNull String path, int accessMode) throws IOException {
+        return null;
+    }
+
+    /**
+     * {@link ParcelFileDescriptor} variant of {@link #loadAsset(String, int)}.
+     *
+     * @param path the asset path to load
+     * @return null if resolution should try to find an entry inside the {@link ResourcesProvider}
+     */
+    @Nullable
+    default ParcelFileDescriptor loadAssetFd(@NonNull String path) throws IOException {
+        return null;
+    }
+}
diff --git a/core/java/android/content/res/loader/ResourceLoaderManager.java b/core/java/android/content/res/loader/ResourceLoaderManager.java
new file mode 100644
index 0000000..ddbfa81
--- /dev/null
+++ b/core/java/android/content/res/loader/ResourceLoaderManager.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2019 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.loader;
+
+import android.annotation.Nullable;
+import android.content.res.ApkAssets;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.ResourcesImpl;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public class ResourceLoaderManager {
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final List<Pair<ResourceLoader, ResourcesProvider>> mResourceLoaders =
+            new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private ResourcesImpl mResourcesImpl;
+
+    public ResourceLoaderManager(ResourcesImpl resourcesImpl) {
+        this.mResourcesImpl = resourcesImpl;
+        this.mResourcesImpl.getAssets().setResourceLoaderManager(this);
+    }
+
+    /**
+     * Copies the list to ensure that ongoing mutations don't affect the list if it's being used
+     * as a search set.
+     *
+     * @see Resources#getLoaders()
+     */
+    public List<Pair<ResourceLoader, ResourcesProvider>> getLoaders() {
+        synchronized (mLock) {
+            return new ArrayList<>(mResourceLoaders);
+        }
+    }
+
+    /**
+     * Returns a list for searching for a loader. Locks and copies the list to ensure that
+     * ongoing mutations don't affect the search set.
+     */
+    public List<Pair<ResourceLoader, ResourcesProvider>> getInternalList() {
+        synchronized (mLock) {
+            return new ArrayList<>(mResourceLoaders);
+        }
+    }
+
+    /**
+     * TODO(b/136251855): Consider optional boolean ignoreConfigurations to allow ResourceLoader
+     * to override every configuration in the target package
+     *
+     * @see Resources#addLoader(ResourceLoader, ResourcesProvider)
+     */
+    public void addLoader(ResourceLoader resourceLoader, ResourcesProvider resourcesProvider,
+            int index) {
+        synchronized (mLock) {
+            for (int listIndex = 0; listIndex < mResourceLoaders.size(); listIndex++) {
+                if (Objects.equals(mResourceLoaders.get(listIndex).first, resourceLoader)) {
+                    throw new IllegalArgumentException("Cannot add the same ResourceLoader twice");
+                }
+            }
+
+            mResourceLoaders.add(index, Pair.create(resourceLoader, resourcesProvider));
+            updateLoaders();
+        }
+    }
+
+    /**
+     * @see Resources#removeLoader(ResourceLoader)
+     */
+    public int removeLoader(ResourceLoader resourceLoader) {
+        synchronized (mLock) {
+            int indexOfLoader = -1;
+
+            for (int index = 0; index < mResourceLoaders.size(); index++) {
+                if (mResourceLoaders.get(index).first == resourceLoader) {
+                    indexOfLoader = index;
+                    break;
+                }
+            }
+
+            if (indexOfLoader < 0) {
+                return indexOfLoader;
+            }
+
+            mResourceLoaders.remove(indexOfLoader);
+            updateLoaders();
+            return indexOfLoader;
+        }
+    }
+
+    /**
+     * @see Resources#setLoaders(List)
+     */
+    public void setLoaders(
+            @Nullable List<Pair<ResourceLoader, ResourcesProvider>> newLoadersAndProviders) {
+        synchronized (mLock) {
+            if (ArrayUtils.isEmpty(newLoadersAndProviders)) {
+                mResourceLoaders.clear();
+                updateLoaders();
+                return;
+            }
+
+            int size = newLoadersAndProviders.size();
+            for (int newIndex = 0; newIndex < size; newIndex++) {
+                ResourceLoader resourceLoader = newLoadersAndProviders.get(newIndex).first;
+                for (int oldIndex = 0; oldIndex < mResourceLoaders.size(); oldIndex++) {
+                    if (Objects.equals(mResourceLoaders.get(oldIndex).first, resourceLoader)) {
+                        throw new IllegalArgumentException(
+                                "Cannot add the same ResourceLoader twice");
+                    }
+                }
+            }
+
+            mResourceLoaders.clear();
+            mResourceLoaders.addAll(newLoadersAndProviders);
+
+            updateLoaders();
+        }
+    }
+
+    /**
+     * Swap the tracked {@link ResourcesImpl} and reattach any loaders to it.
+     */
+    public void onImplUpdate(ResourcesImpl resourcesImpl) {
+        synchronized (mLock) {
+            this.mResourcesImpl = resourcesImpl;
+            updateLoaders();
+        }
+    }
+
+    private void updateLoaders() {
+        synchronized (mLock) {
+            AssetManager assetManager = mResourcesImpl.getAssets();
+            ApkAssets[] existingApkAssets = assetManager.getApkAssets();
+            int baseApkAssetsSize = 0;
+            for (int index = existingApkAssets.length - 1; index >= 0; index--) {
+                // Loaders are always last, so the first non-loader is the end of the base assets
+                if (!existingApkAssets[index].isForLoader()) {
+                    baseApkAssetsSize = index + 1;
+                    break;
+                }
+            }
+
+            List<ApkAssets> newAssets = new ArrayList<>();
+            for (int index = 0; index < baseApkAssetsSize; index++) {
+                newAssets.add(existingApkAssets[index]);
+            }
+
+            int size = mResourceLoaders.size();
+            for (int index = 0; index < size; index++) {
+                ApkAssets apkAssets = mResourceLoaders.get(index).second.getApkAssets();
+                newAssets.add(apkAssets);
+            }
+
+            assetManager.setApkAssets(newAssets.toArray(new ApkAssets[0]), true);
+
+            // Short of resolving every resource, it's too difficult to determine what has changed
+            // when a resource loader is changed, so just clear everything.
+            mResourcesImpl.clearAllCaches();
+        }
+    }
+}
diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java
new file mode 100644
index 0000000..050aeb7
--- /dev/null
+++ b/core/java/android/content/res/loader/ResourcesProvider.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2019 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.loader;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.ApkAssets;
+import android.content.res.Resources;
+import android.os.ParcelFileDescriptor;
+import android.os.SharedMemory;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Provides methods to load resources from an .apk or .arsc file to pass to
+ * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}.
+ *
+ * It is the responsibility of the app to close any instances.
+ */
+public final class ResourcesProvider implements AutoCloseable, Closeable {
+
+    /**
+     * Contains no data, assuming that any resource loading behavior will be handled in the
+     * corresponding {@link ResourceLoader}.
+     */
+    @NonNull
+    public static ResourcesProvider empty() {
+        return new ResourcesProvider(ApkAssets.loadEmptyForLoader());
+    }
+
+    /**
+     * Read from an .apk file descriptor.
+     *
+     * The file descriptor is duplicated and the one passed in may be closed by the application
+     * at any time.
+     */
+    @NonNull
+    public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadApkForLoader(fileDescriptor.getFileDescriptor()));
+    }
+
+    /**
+     * Read from an .apk file representation in memory.
+     */
+    @NonNull
+    public static ResourcesProvider loadFromApk(@NonNull SharedMemory sharedMemory)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadApkForLoader(sharedMemory.getFileDescriptor()));
+    }
+
+    /**
+     * Read from an .arsc file descriptor.
+     *
+     * The file descriptor is duplicated and the one passed in may be closed by the application
+     * at any time.
+     */
+    @NonNull
+    public static ResourcesProvider loadFromArsc(@NonNull ParcelFileDescriptor fileDescriptor)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadArscForLoader(fileDescriptor.getFileDescriptor()));
+    }
+
+    /**
+     * Read from an .arsc file representation in memory.
+     */
+    @NonNull
+    public static ResourcesProvider loadFromArsc(@NonNull SharedMemory sharedMemory)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadArscForLoader(sharedMemory.getFileDescriptor()));
+    }
+
+    /**
+     * Read from a split installed alongside the application, which may not have been
+     * loaded initially because the application requested isolated split loading.
+     */
+    @NonNull
+    public static ResourcesProvider loadFromSplit(@NonNull Context context,
+            @NonNull String splitName) throws IOException {
+        ApplicationInfo appInfo = context.getApplicationInfo();
+        int splitIndex = ArrayUtils.indexOf(appInfo.splitNames, splitName);
+        if (splitIndex < 0) {
+            throw new IllegalArgumentException("Split " + splitName + " not found");
+        }
+
+        String splitPath = appInfo.getSplitCodePaths()[splitIndex];
+        return new ResourcesProvider(ApkAssets.loadApkForLoader(splitPath));
+    }
+
+
+    @NonNull
+    private final ApkAssets mApkAssets;
+
+    private ResourcesProvider(@NonNull ApkAssets apkAssets) {
+        this.mApkAssets = apkAssets;
+    }
+
+    /** @hide */
+    @NonNull
+    public ApkAssets getApkAssets() {
+        return mApkAssets;
+    }
+
+    @Override
+    public void close() {
+        try {
+            mApkAssets.close();
+        } catch (Throwable ignored) {
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        close();
+        super.finalize();
+    }
+}
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index bd4862d..6370253 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -16,6 +16,7 @@
 
 #define ATRACE_TAG ATRACE_TAG_RESOURCES
 
+#include "android-base/logging.h"
 #include "android-base/macros.h"
 #include "android-base/stringprintf.h"
 #include "android-base/unique_fd.h"
@@ -32,7 +33,7 @@
 namespace android {
 
 static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system,
-                        jboolean force_shared_lib, jboolean overlay) {
+                        jboolean force_shared_lib, jboolean overlay, jboolean for_loader) {
   ScopedUtfChars path(env, java_path);
   if (path.c_str() == nullptr) {
     return 0;
@@ -46,7 +47,7 @@
   } else if (force_shared_lib) {
     apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system);
   } else {
-    apk_assets = ApkAssets::Load(path.c_str(), system);
+    apk_assets = ApkAssets::Load(path.c_str(), system, for_loader);
   }
 
   if (apk_assets == nullptr) {
@@ -58,7 +59,8 @@
 }
 
 static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor,
-                              jstring friendly_name, jboolean system, jboolean force_shared_lib) {
+                              jstring friendly_name, jboolean system, jboolean force_shared_lib,
+                              jboolean for_loader) {
   ScopedUtfChars friendly_name_utf8(env, friendly_name);
   if (friendly_name_utf8.c_str() == nullptr) {
     return 0;
@@ -80,7 +82,9 @@
 
   std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadFromFd(std::move(dup_fd),
                                                                       friendly_name_utf8.c_str(),
-                                                                      system, force_shared_lib);
+                                                                      system, force_shared_lib,
+                                                                      for_loader);
+
   if (apk_assets == nullptr) {
     std::string error_msg = base::StringPrintf("Failed to load asset path %s from fd %d",
                                                friendly_name_utf8.c_str(), dup_fd.get());
@@ -90,6 +94,60 @@
   return reinterpret_cast<jlong>(apk_assets.release());
 }
 
+static jlong NativeLoadArsc(JNIEnv* env, jclass /*clazz*/, jstring java_path,
+                            jboolean for_loader) {
+  ScopedUtfChars path(env, java_path);
+  if (path.c_str() == nullptr) {
+    return 0;
+  }
+
+  ATRACE_NAME(base::StringPrintf("LoadApkAssetsArsc(%s)", path.c_str()).c_str());
+
+  std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadArsc(path.c_str(), for_loader);
+
+  if (apk_assets == nullptr) {
+    std::string error_msg = base::StringPrintf("Failed to load asset path %s", path.c_str());
+    jniThrowException(env, "java/io/IOException", error_msg.c_str());
+    return 0;
+  }
+  return reinterpret_cast<jlong>(apk_assets.release());
+}
+
+static jlong NativeLoadArscFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor,
+                                  jstring friendly_name, jboolean for_loader) {
+  ScopedUtfChars friendly_name_utf8(env, friendly_name);
+  if (friendly_name_utf8.c_str() == nullptr) {
+    return 0;
+  }
+
+  int fd = jniGetFDFromFileDescriptor(env, file_descriptor);
+  ATRACE_NAME(base::StringPrintf("LoadApkAssetsArscFd(%d)", fd).c_str());
+  if (fd < 0) {
+    jniThrowException(env, "java/lang/IllegalArgumentException", "Bad FileDescriptor");
+    return 0;
+  }
+
+  unique_fd dup_fd(::fcntl(fd, F_DUPFD_CLOEXEC, 0));
+  if (dup_fd < 0) {
+    jniThrowIOException(env, errno);
+    return 0;
+  }
+
+  std::unique_ptr<const ApkAssets> apk_assets =
+      ApkAssets::LoadArsc(std::move(dup_fd), friendly_name_utf8.c_str(), for_loader);
+  if (apk_assets == nullptr) {
+    std::string error_msg = base::StringPrintf("Failed to load asset path from fd %d", fd);
+    jniThrowException(env, "java/io/IOException", error_msg.c_str());
+    return 0;
+  }
+  return reinterpret_cast<jlong>(apk_assets.release());
+}
+
+static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jboolean for_loader) {
+  std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadEmpty(for_loader);
+  return reinterpret_cast<jlong>(apk_assets.release());
+}
+
 static void NativeDestroy(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) {
   delete reinterpret_cast<ApkAssets*>(ptr);
 }
@@ -138,9 +196,13 @@
 
 // JNI registration.
 static const JNINativeMethod gApkAssetsMethods[] = {
-    {"nativeLoad", "(Ljava/lang/String;ZZZ)J", (void*)NativeLoad},
-    {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZ)J",
+    {"nativeLoad", "(Ljava/lang/String;ZZZZ)J", (void*)NativeLoad},
+    {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZZ)J",
         (void*)NativeLoadFromFd},
+    {"nativeLoadArsc", "(Ljava/lang/String;Z)J", (void*)NativeLoadArsc},
+    {"nativeLoadArscFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;Z)J",
+        (void*)NativeLoadArscFromFd},
+    {"nativeLoadEmpty", "(Z)J", (void*)NativeLoadEmpty},
     {"nativeDestroy", "(J)V", (void*)NativeDestroy},
     {"nativeGetAssetPath", "(J)Ljava/lang/String;", (void*)NativeGetAssetPath},
     {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index daf33f6..af47cb6d 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -763,6 +763,41 @@
   return reinterpret_cast<jlong>(xml_tree.release());
 }
 
+static jlong NativeOpenXmlAssetFd(JNIEnv* env, jobject /*clazz*/, jlong ptr, int jcookie,
+                                  jobject file_descriptor) {
+  int fd = jniGetFDFromFileDescriptor(env, file_descriptor);
+  ATRACE_NAME(base::StringPrintf("AssetManager::OpenXmlAssetFd(%d)", fd).c_str());
+  if (fd < 0) {
+    jniThrowException(env, "java/lang/IllegalArgumentException", "Bad FileDescriptor");
+    return 0;
+  }
+
+  base::unique_fd dup_fd(::fcntl(fd, F_DUPFD_CLOEXEC, 0));
+  if (dup_fd < 0) {
+    jniThrowIOException(env, errno);
+    return 0;
+  }
+
+  std::unique_ptr<Asset>
+      asset(Asset::createFromFd(dup_fd.release(), nullptr, Asset::AccessMode::ACCESS_BUFFER));
+
+  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
+  ApkAssetsCookie cookie = JavaCookieToApkAssetsCookie(jcookie);
+
+  // May be nullptr.
+  const DynamicRefTable* dynamic_ref_table = assetmanager->GetDynamicRefTableForCookie(cookie);
+
+  std::unique_ptr<ResXMLTree> xml_tree = util::make_unique<ResXMLTree>(dynamic_ref_table);
+  status_t err = xml_tree->setTo(asset->getBuffer(true), asset->getLength(), true);
+  asset.reset();
+
+  if (err != NO_ERROR) {
+    jniThrowException(env, "java/io/FileNotFoundException", "Corrupt XML binary file");
+    return 0;
+  }
+  return reinterpret_cast<jlong>(xml_tree.release());
+}
+
 static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
                                    jshort density, jobject typed_value,
                                    jboolean resolve_references) {
@@ -1564,6 +1599,7 @@
     {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;",
      (void*)NativeOpenNonAssetFd},
     {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset},
+    {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd},
 
     // AssetManager resource methods.
     {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I", (void*)NativeGetResourceValue},
diff --git a/core/tests/ResourceLoaderTests/Android.bp b/core/tests/ResourceLoaderTests/Android.bp
new file mode 100644
index 0000000..53db832
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/Android.bp
@@ -0,0 +1,63 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_test {
+    name: "FrameworksResourceLoaderTests",
+    srcs: [
+        "src/**/*.kt"
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.espresso.core",
+        "androidx.test.ext.junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "mockito-target-minus-junit4",
+        "truth-prebuilt",
+    ],
+    resource_zips: [ ":FrameworksResourceLoaderTestsAssets" ],
+    test_suites: ["device-tests"],
+    sdk_version: "test_current",
+    aaptflags: [
+        "--no-compress",
+    ],
+    data: [
+        ":FrameworksResourceLoaderTestsOverlay",
+        ":FrameworksResourceLoaderTestsSplitOne",
+        ":FrameworksResourceLoaderTestsSplitTwo",
+    ],
+    java_resources: [ "NonAsset.txt" ]
+}
+
+filegroup {
+    name: "FrameworksResourceLoaderTestsResources",
+    srcs: ["resources"],
+}
+
+genrule {
+    name: "FrameworksResourceLoaderTestsAssets",
+    srcs: [
+        ":framework-res",
+        ":FrameworksResourceLoaderTestsResources",
+    ],
+    tools: [ ":aapt2", ":soong_zip" ],
+    tool_files: [ "resources/compileAndLink.sh" ],
+    cmd: "$(location resources/compileAndLink.sh) $(location :aapt2) $(location :soong_zip) $(genDir) $(in) $(in)",
+    out: [ "out.zip" ]
+}
diff --git a/core/tests/ResourceLoaderTests/AndroidManifest.xml b/core/tests/ResourceLoaderTests/AndroidManifest.xml
new file mode 100644
index 0000000..00b4ccb
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<!-- Split loading is tested separately, so this must be marked isolated -->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.res.loader.test"
+    android:isolatedSplits="true"
+    >
+
+    <uses-sdk android:minSdkVersion="29"/>
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+
+        <activity
+            android:name=".TestActivity"
+            android:configChanges="orientation"
+            />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="ResourceLoaderTests"
+        android:targetPackage="android.content.res.loader.test"
+        />
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/AndroidTest.xml b/core/tests/ResourceLoaderTests/AndroidTest.xml
new file mode 100644
index 0000000..702151d
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<configuration description="Test module config for ResourceLoaderTests">
+    <option name="test-tag" value="ResourceLoaderTests" />
+
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="cleanup-apks" value="true" />
+        <!-- The following value cannot be multi-line as whitespace is parsed by the installer -->
+        <option name="split-apk-file-names"
+            value="FrameworksResourceLoaderTests.apk,FrameworksResourceLoaderTestsSplitOne.apk,FrameworksResourceLoaderTestsSplitTwo.apk" />
+        <option name="test-file-name" value="FrameworksResourceLoaderTestsOverlay.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command"
+            value="cmd overlay disable android.content.res.loader.test.overlay" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.content.res.loader.test" />
+    </test>
+</configuration>
diff --git a/core/tests/ResourceLoaderTests/NonAsset.txt b/core/tests/ResourceLoaderTests/NonAsset.txt
new file mode 100644
index 0000000..5c0b2cc
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/NonAsset.txt
@@ -0,0 +1 @@
+Outside assets directory
diff --git a/core/tests/ResourceLoaderTests/SplitOne/Android.bp b/core/tests/ResourceLoaderTests/SplitOne/Android.bp
new file mode 100644
index 0000000..897897f
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/SplitOne/Android.bp
@@ -0,0 +1,19 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_test_helper_app {
+    name: "FrameworksResourceLoaderTestsSplitOne"
+}
diff --git a/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml b/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml
new file mode 100644
index 0000000..b14bd86
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.res.loader.test"
+    split="split_one"
+    >
+
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
+    <application android:hasCode="false" />
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml b/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml
new file mode 100644
index 0000000..3c215eb
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+    <public type="string" name="split_overlaid" id="0x7f040001" />
+    <string name="split_overlaid">Split ONE Overlaid</string>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/assets/Asset.txt b/core/tests/ResourceLoaderTests/assets/Asset.txt
new file mode 100644
index 0000000..03f9a0f
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/assets/Asset.txt
@@ -0,0 +1 @@
+In assets directory
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar
new file mode 100644
index 0000000..a12e33a
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar b/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar
new file mode 100644
index 0000000..182cbab
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar
new file mode 100644
index 0000000..e6b5f15b
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar
new file mode 100644
index 0000000..e9c743c
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar
new file mode 100644
index 0000000..cd05360
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar
new file mode 100644
index 0000000..dc8aa90
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar
new file mode 100644
index 0000000..8a672ba
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar
new file mode 100644
index 0000000..56f3d1e
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar
new file mode 100644
index 0000000..663d312
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-test.jar b/core/tests/ResourceLoaderTests/lib/kotlin-test.jar
new file mode 100644
index 0000000..5f6e4b8
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/lib/kotlin-test.jar
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/overlay/Android.bp b/core/tests/ResourceLoaderTests/overlay/Android.bp
new file mode 100644
index 0000000..63e7e61
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/overlay/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+android_test {
+    name: "FrameworksResourceLoaderTestsOverlay",
+    sdk_version: "current",
+
+    aaptflags: ["--no-resource-removal"],
+}
diff --git a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml b/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
new file mode 100644
index 0000000..942f7da
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.res.loader.test.overlay"
+    >
+
+    <application android:hasCode="false" />
+
+    <overlay android:targetPackage="android.content.res.loader.test" />
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
new file mode 100644
index 0000000..348bb35
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+
+    <string name="loader_path_change_test">Overlaid</string>
+
+</resources>
diff --git a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png
new file mode 100644
index 0000000..efd71ee
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml
new file mode 100644
index 0000000..d1211c5
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<color
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#B2D2F2"
+    />
diff --git a/core/tests/ResourceLoaderTests/res/layout/layout.xml b/core/tests/ResourceLoaderTests/res/layout/layout.xml
new file mode 100644
index 0000000..d59059b
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/res/layout/layout.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
+
diff --git a/core/tests/ResourceLoaderTests/res/values/strings.xml b/core/tests/ResourceLoaderTests/res/values/strings.xml
new file mode 100644
index 0000000..28b8f73
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+
+    <string name="loader_path_change_test">Not overlaid</string>
+    <string name="split_overlaid">Not overlaid</string>
+
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml b/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml
new file mode 100644
index 0000000..5dd8a96
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.res.loader.test"
+    >
+
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
+    <application/>
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml b/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml
new file mode 100644
index 0000000..5a92ae9
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<!-- Mocks the framework package name so that AAPT2 assigns the correct package -->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android"
+    >
+
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
+    <application/>
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/resources/compileAndLink.sh b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh
new file mode 100755
index 0000000..885f681
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+# Copyright (C) 2019 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.
+
+aapt2=$1
+soong_zip=$2
+genDir=$3
+FRAMEWORK_RES_APK=$4
+inDir=$5
+
+# (String name, boolean retainFiles = false, String... files)
+function compileAndLink {
+    moduleName=$1
+    mkdir "$genDir"/out/"$moduleName"
+
+    args=""
+    for arg in "${@:4}"; do
+        if [[ $arg == res* ]]; then
+            args="$args $inDir/$arg"
+        else
+            args="$args $arg"
+        fi
+    done
+
+    $aapt2 compile -o "$genDir"/out/"$moduleName" $args
+
+    $aapt2 link \
+        -I "$FRAMEWORK_RES_APK" \
+        --manifest "$inDir"/"$3" \
+        -o "$genDir"/out/"$moduleName"/apk.apk \
+        "$genDir"/out/"$moduleName"/*.flat \
+        --no-compress
+
+    unzip -qq "$genDir"/out/"$moduleName"/apk.apk -d "$genDir"/out/"$moduleName"/unzip
+
+    if [[ "$2" == "APK_WITHOUT_FILE" || "$2" == "BOTH_WITHOUT_FILE" ]]; then
+        zip -q -d "$genDir"/out/"$moduleName"/apk.apk "res/*"
+        cp "$genDir"/out/"$moduleName"/apk.apk "$genDir"/output/raw/"$moduleName"Apk.apk
+    elif [[ "$2" == "APK" || "$2" == "BOTH" ]]; then
+        cp "$genDir"/out/"$moduleName"/apk.apk "$genDir"/output/raw/"$moduleName"Apk.apk
+    fi
+
+    if [[ "$2" == "ARSC" || "$2" == "BOTH" || "$2" == "BOTH_WITHOUT_FILE" ]]; then
+        zip -d "$genDir"/out/"$moduleName"/apk.apk "res/*"
+        cp "$genDir"/out/"$moduleName"/unzip/resources.arsc "$genDir"/output/raw/"$moduleName"Arsc.arsc
+    fi
+}
+
+rm -r "$genDir"/out
+rm -r "$genDir"/output
+rm -r "$genDir"/temp
+
+mkdir "$genDir"/out
+mkdir -p "$genDir"/output/raw
+mkdir -p "$genDir"/temp/res/drawable-nodpi
+mkdir -p "$genDir"/temp/res/layout
+
+compileAndLink stringOne BOTH AndroidManifestFramework.xml res/values/string_one.xml
+compileAndLink stringTwo BOTH AndroidManifestFramework.xml res/values/string_two.xml
+
+compileAndLink dimenOne BOTH AndroidManifestFramework.xml res/values/dimen_one.xml
+compileAndLink dimenTwo BOTH AndroidManifestFramework.xml res/values/dimen_two.xml
+
+compileAndLink drawableMdpiWithoutFile BOTH_WITHOUT_FILE AndroidManifestFramework.xml res/values/drawable_one.xml res/drawable-mdpi/ic_delete.png
+compileAndLink drawableMdpiWithFile APK AndroidManifestFramework.xml res/values/drawable_one.xml res/drawable-mdpi/ic_delete.png
+
+compileAndLink layoutWithoutFile BOTH_WITHOUT_FILE AndroidManifestFramework.xml res/values/activity_list_item_id.xml res/layout/activity_list_item.xml
+compileAndLink layoutWithFile APK AndroidManifestFramework.xml res/values/activity_list_item_id.xml res/layout/activity_list_item.xml
+
+cp -f "$inDir"/res/layout/layout_one.xml "$genDir"/temp/res/layout/layout.xml
+compileAndLink layoutOne ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml
+cp -f "$genDir"/out/layoutOne/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutOne.xml
+
+cp -f "$inDir"/res/layout/layout_two.xml "$genDir"/temp/res/layout/layout.xml
+compileAndLink layoutTwo ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml
+cp -f "$genDir"/out/layoutTwo/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutTwo.xml
+
+drawableNoDpi="/res/drawable-nodpi"
+inDirDrawableNoDpi="$inDir$drawableNoDpi"
+
+cp -f "$inDirDrawableNoDpi"/nonAssetDrawableOne.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml
+compileAndLink nonAssetDrawableOne ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml
+cp -f "$genDir"/out/nonAssetDrawableOne/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableOne.xml
+
+cp -f "$inDirDrawableNoDpi"/nonAssetDrawableTwo.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml
+compileAndLink nonAssetDrawableTwo ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml
+cp -f "$genDir"/out/nonAssetDrawableTwo/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableTwo.xml
+
+cp -f "$inDirDrawableNoDpi"/nonAssetBitmapGreen.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png
+compileAndLink nonAssetBitmapGreen BOTH AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml
+cp -f "$genDir"/out/nonAssetBitmapGreen/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapGreen.png
+
+cp -f "$inDirDrawableNoDpi"/nonAssetBitmapBlue.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png
+compileAndLink nonAssetBitmapBlue ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml
+cp -f "$genDir"/out/nonAssetBitmapBlue/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapBlue.png
+
+$soong_zip -o "$genDir"/out.zip -C "$genDir"/output/ -D "$genDir"/output/
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png b/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png
new file mode 100644
index 0000000..f3e53d7
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png
new file mode 100644
index 0000000..5231d17
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png
new file mode 100644
index 0000000..671d6d0
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml
new file mode 100644
index 0000000..f1a93d2
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<color
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#A3C3E3"
+    />
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml
new file mode 100644
index 0000000..7c455a5
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<color
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#3A3C3E"
+    />
diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml b/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml
new file mode 100644
index 0000000..d59059b
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
+
diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml
new file mode 100644
index 0000000..ede3838
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
+
diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml
new file mode 100644
index 0000000..d8bff90
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
+
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml
new file mode 100644
index 0000000..a552431
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="layout" name="activity_list_item" id="0x01090000" />
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml
new file mode 100644
index 0000000..69ecf23
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="dimen" name="app_icon_size" id="0x01050000" />
+    <dimen name="app_icon_size">564716dp</dimen>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml
new file mode 100644
index 0000000..4d55def
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="dimen" name="app_icon_size" id="0x01050000" />
+    <dimen name="app_icon_size">565717dp</dimen>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml
new file mode 100644
index 0000000..b5b4dfd
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="drawable" name="ic_delete" id="0x0108001d" />
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml
new file mode 100644
index 0000000..4962a07
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+    <public type="layout" name="layout" id="0x7f020000" />
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml
new file mode 100644
index 0000000..38b152b
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+    <public type="drawable" name="non_asset_bitmap" id="0x7f010000" />
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml
new file mode 100644
index 0000000..bdd6f58
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+    <public type="drawable" name="non_asset_drawable" id="0x7f010001" />
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml
new file mode 100644
index 0000000..4fc5272
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="string" name="cancel" id="0x01040000" />
+    <string name="cancel">SomeRidiculouslyUnlikelyStringOne</string>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml
new file mode 100644
index 0000000..3604d7b
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<resources>
+    <public type="string" name="cancel" id="0x01040000" />
+    <string name="cancel">SomeRidiculouslyUnlikelyStringTwo</string>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/splits/Android.bp b/core/tests/ResourceLoaderTests/splits/Android.bp
new file mode 100644
index 0000000..4582808
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/splits/Android.bp
@@ -0,0 +1,19 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_test_helper_app {
+    name: "FrameworksResourceLoaderTestsSplitTwo"
+}
diff --git a/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml
new file mode 100644
index 0000000..aad8c27
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.res.loader.test"
+    split="split_two"
+    >
+
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
+    <application android:hasCode="false" />
+
+</manifest>
diff --git a/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml b/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml
new file mode 100644
index 0000000..a367063
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.
+  -->
+
+<resources>
+    <public type="string" name="split_overlaid" id="0x7f040001" />
+    <string name="split_overlaid">Split TWO Overlaid</string>
+</resources>
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt
new file mode 100644
index 0000000..b1bdc96
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.res.loader.DirectoryResourceLoader
+import android.content.res.loader.ResourceLoader
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import java.io.File
+
+class DirectoryResourceLoaderTest : ResourceLoaderTestBase() {
+
+    @get:Rule
+    val testName = TestName()
+
+    private lateinit var testDir: File
+    private lateinit var loader: ResourceLoader
+
+    @Before
+    fun setUpTestDir() {
+        testDir = context.filesDir.resolve("DirectoryResourceLoaderTest_${testName.methodName}")
+        loader = DirectoryResourceLoader(testDir)
+    }
+
+    @After
+    fun deleteTestFiles() {
+        testDir.deleteRecursively()
+    }
+
+    @Test
+    fun loadDrawableXml() {
+        "nonAssetDrawableOne" writeTo "res/drawable-nodpi-v4/non_asset_drawable.xml"
+        val provider = openArsc("nonAssetDrawableOne")
+
+        fun getValue() = (resources.getDrawable(R.drawable.non_asset_drawable) as ColorDrawable)
+                .color
+
+        assertThat(getValue()).isEqualTo(Color.parseColor("#B2D2F2"))
+
+        addLoader(loader to provider)
+
+        assertThat(getValue()).isEqualTo(Color.parseColor("#A3C3E3"))
+    }
+
+    @Test
+    fun loadDrawableBitmap() {
+        "nonAssetBitmapGreen" writeTo "res/drawable-nodpi-v4/non_asset_bitmap.png"
+        val provider = openArsc("nonAssetBitmapGreen")
+
+        fun getValue() = (resources.getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable)
+                .bitmap.getColor(0, 0).toArgb()
+
+        assertThat(getValue()).isEqualTo(Color.RED)
+
+        addLoader(loader to provider)
+
+        assertThat(getValue()).isEqualTo(Color.GREEN)
+    }
+
+    @Test
+    fun loadXml() {
+        "layoutOne" writeTo "res/layout/layout.xml"
+        val provider = openArsc("layoutOne")
+
+        fun getValue() = resources.getLayout(R.layout.layout).advanceToRoot().name
+
+        assertThat(getValue()).isEqualTo("FrameLayout")
+
+        addLoader(loader to provider)
+
+        assertThat(getValue()).isEqualTo("RelativeLayout")
+    }
+
+    private infix fun String.writeTo(path: String) {
+        val testFile = testDir.resolve(path)
+        testFile.parentFile!!.mkdirs()
+        resources.openRawResource(rawFile(this))
+                .copyTo(testFile.outputStream())
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt
new file mode 100644
index 0000000..a6a8378
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.res.AssetManager
+import android.content.res.loader.DirectoryResourceLoader
+import android.content.res.loader.ResourceLoader
+import android.content.res.loader.ResourcesProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.nio.file.Paths
+
+@RunWith(Parameterized::class)
+class ResourceLoaderAssetTest : ResourceLoaderTestBase() {
+
+    companion object {
+        private const val BASE_TEST_PATH = "android/content/res/loader/test/file.txt"
+        private const val TEST_TEXT = "some text"
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun parameters(): Array<Array<out Any?>> {
+            val fromInputStream: ResourceLoader.(String) -> Any? = {
+                loadAsset(eq(it), anyInt())
+            }
+
+            val fromFileDescriptor: ResourceLoader.(String) -> Any? = {
+                loadAssetFd(eq(it))
+            }
+
+            val openAsset: AssetManager.() -> String? = {
+                open(BASE_TEST_PATH).reader().readText()
+            }
+
+            val openNonAsset: AssetManager.() -> String? = {
+                openNonAssetFd(BASE_TEST_PATH).readText()
+            }
+
+            return arrayOf(
+                    arrayOf("assets", fromInputStream, openAsset),
+                    arrayOf("", fromFileDescriptor, openNonAsset)
+            )
+        }
+    }
+
+    @get:Rule
+    val testName = TestName()
+
+    @JvmField
+    @field:Parameterized.Parameter(0)
+    var prefix: String? = null
+
+    @field:Parameterized.Parameter(1)
+    lateinit var loadAssetFunction: ResourceLoader.(String) -> Any?
+
+    @field:Parameterized.Parameter(2)
+    lateinit var openAssetFunction: AssetManager.() -> String?
+
+    private val testPath: String
+        get() = Paths.get(prefix.orEmpty(), BASE_TEST_PATH).toString()
+
+    private fun ResourceLoader.loadAsset() = loadAssetFunction(testPath)
+
+    private fun AssetManager.openAsset() = openAssetFunction()
+
+    private lateinit var testDir: File
+
+    @Before
+    fun setUpTestDir() {
+        testDir = context.filesDir.resolve("DirectoryResourceLoaderTest_${testName.methodName}")
+        testDir.resolve(testPath).apply { parentFile!!.mkdirs() }.writeText(TEST_TEXT)
+    }
+
+    @Test
+    fun multipleLoadersSearchesBackwards() {
+        // DirectoryResourceLoader relies on a private field and can't be spied directly, so wrap it
+        val loader = DirectoryResourceLoader(testDir)
+        val loaderWrapper = mock(ResourceLoader::class.java).apply {
+            doAnswer { loader.loadAsset(it.arguments[0] as String, it.arguments[1] as Int) }
+                    .`when`(this).loadAsset(anyString(), anyInt())
+            doAnswer { loader.loadAssetFd(it.arguments[0] as String) }
+                    .`when`(this).loadAssetFd(anyString())
+        }
+
+        val one = loaderWrapper to ResourcesProvider.empty()
+        val two = mockLoader {
+            doReturn(null).`when`(it).loadAsset()
+        }
+
+        addLoader(one, two)
+
+        assertOpenedAsset()
+        inOrder(two.first, one.first).apply {
+            verify(two.first).loadAsset()
+            verify(one.first).loadAsset()
+        }
+    }
+
+    @Test(expected = FileNotFoundException::class)
+    fun failToFindThrowsFileNotFound() {
+        val one = mockLoader {
+            doReturn(null).`when`(it).loadAsset()
+        }
+        val two = mockLoader {
+            doReturn(null).`when`(it).loadAsset()
+        }
+
+        addLoader(one, two)
+
+        assertOpenedAsset()
+    }
+
+    @Test
+    fun throwingIOExceptionIsSkipped() {
+        val one = DirectoryResourceLoader(testDir) to ResourcesProvider.empty()
+        val two = mockLoader {
+            doAnswer { throw IOException() }.`when`(it).loadAsset()
+        }
+
+        addLoader(one, two)
+
+        assertOpenedAsset()
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun throwingNonIOExceptionCausesFailure() {
+        val one = DirectoryResourceLoader(testDir) to ResourcesProvider.empty()
+        val two = mockLoader {
+            doAnswer { throw IllegalStateException() }.`when`(it).loadAsset()
+        }
+
+        addLoader(one, two)
+
+        assertOpenedAsset()
+    }
+
+    private fun assertOpenedAsset() {
+        assertThat(resources.assets.openAsset()).isEqualTo(TEST_TEXT)
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
new file mode 100644
index 0000000..e01e254
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Color
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import android.widget.FrameLayout
+import androidx.test.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
+import androidx.test.runner.lifecycle.Stage
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.Arrays
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
+import java.util.concurrent.FutureTask
+import java.util.concurrent.TimeUnit
+
+// @Ignore("UiAutomation is crashing with not connected, not sure why")
+@RunWith(Parameterized::class)
+class ResourceLoaderChangesTest : ResourceLoaderTestBase() {
+
+    companion object {
+        private const val TIMEOUT = 30L
+        private const val OVERLAY_PACKAGE = "android.content.res.loader.test.overlay"
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = arrayOf(DataType.APK, DataType.ARSC)
+    }
+
+    @field:Parameterized.Parameter(0)
+    override lateinit var dataType: DataType
+
+    @get:Rule
+    val activityRule: ActivityTestRule<TestActivity> =
+            ActivityTestRule<TestActivity>(TestActivity::class.java, false, true)
+
+    // Redirect to the Activity's resources
+    override val resources: Resources
+        get() = activityRule.getActivity().resources
+
+    private val activity: TestActivity
+        get() = activityRule.getActivity()
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Before
+    @After
+    fun disableOverlay() {
+//        enableOverlay(OVERLAY_PACKAGE, false)
+    }
+
+    @Test
+    fun activityRecreate() = verifySameBeforeAndAfter {
+        val oldActivity = activity
+        var newActivity: Activity? = null
+        instrumentation.runOnMainSync { oldActivity.recreate() }
+        instrumentation.waitForIdleSync()
+        instrumentation.runOnMainSync {
+            newActivity = ActivityLifecycleMonitorRegistry.getInstance()
+                    .getActivitiesInStage(Stage.RESUMED)
+                    .single()
+        }
+
+        assertThat(newActivity).isNotNull()
+        assertThat(newActivity).isNotSameAs(oldActivity)
+
+        // Return the new resources to assert on
+        return@verifySameBeforeAndAfter newActivity!!.resources
+    }
+
+    @Test
+    fun activityHandledOrientationChange() = verifySameBeforeAndAfter {
+        val latch = CountDownLatch(1)
+        val oldConfig = Configuration().apply { setTo(resources.configuration) }
+        var changedConfig: Configuration? = null
+
+        activity.callback = object : TestActivity.Callback {
+            override fun onConfigurationChanged(newConfig: Configuration) {
+                changedConfig = newConfig
+                latch.countDown()
+            }
+        }
+
+        val isPortrait = resources.displayMetrics.run { widthPixels < heightPixels }
+        val newRotation = if (isPortrait) {
+            UiAutomation.ROTATION_FREEZE_90
+        } else {
+            UiAutomation.ROTATION_FREEZE_0
+        }
+
+        instrumentation.uiAutomation.setRotation(newRotation)
+
+        assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue()
+        assertThat(changedConfig).isNotEqualTo(oldConfig)
+        return@verifySameBeforeAndAfter activity.resources
+    }
+
+    @Test
+    fun enableOverlayCausingPathChange() = verifySameBeforeAndAfter {
+        assertThat(getString(R.string.loader_path_change_test)).isEqualTo("Not overlaid")
+
+        enableOverlay(OVERLAY_PACKAGE, true)
+
+        assertThat(getString(R.string.loader_path_change_test)).isEqualTo("Overlaid")
+
+        return@verifySameBeforeAndAfter activity.resources
+    }
+
+    @Test
+    fun enableOverlayChildContextUnaffected() {
+        val childContext = activity.createConfigurationContext(Configuration())
+        val childResources = childContext.resources
+        val originalValue = childResources.getString(android.R.string.cancel)
+        assertThat(childResources.getString(R.string.loader_path_change_test))
+                .isEqualTo("Not overlaid")
+
+        verifySameBeforeAndAfter {
+            enableOverlay(OVERLAY_PACKAGE, true)
+            return@verifySameBeforeAndAfter activity.resources
+        }
+
+        // Loader not applied, but overlay change propagated
+        assertThat(childResources.getString(android.R.string.cancel)).isEqualTo(originalValue)
+        assertThat(childResources.getString(R.string.loader_path_change_test))
+                .isEqualTo("Overlaid")
+    }
+
+    // All these tests assert for the exact same loaders/values, so extract that logic out
+    private fun verifySameBeforeAndAfter(block: () -> Resources) {
+        // TODO(chiuwinson): atest doesn't work with @Ignore, UiAutomation not connected error
+        Assume.assumeFalse(true)
+
+        val originalValue = resources.getString(android.R.string.cancel)
+
+        val loader = "stringOne".openLoader()
+        addLoader(loader)
+
+        val oldLoaders = resources.loaders
+        val oldValue = resources.getString(android.R.string.cancel)
+
+        assertThat(oldValue).isNotEqualTo(originalValue)
+
+        val newResources = block()
+
+        val newLoaders = newResources.loaders
+        val newValue = newResources.getString(android.R.string.cancel)
+
+        assertThat(newValue).isEqualTo(oldValue)
+        assertThat(newLoaders).isEqualTo(oldLoaders)
+    }
+
+    // Copied from overlaytests LocalOverlayManager
+    private fun enableOverlay(packageName: String, enable: Boolean) {
+        val executor = Executor { Thread(it).start() }
+        val pattern = (if (enable) "[x]" else "[ ]") + " " + packageName
+        if (executeShellCommand("cmd overlay list").contains(pattern)) {
+            // nothing to do, overlay already in the requested state
+            return
+        }
+
+        val oldApkPaths = resources.assets.apkPaths
+        val task = FutureTask {
+            while (true) {
+                if (!Arrays.equals(oldApkPaths, resources.assets.apkPaths)) {
+                    return@FutureTask true
+                }
+                Thread.sleep(10)
+            }
+
+            @Suppress("UNREACHABLE_CODE")
+            return@FutureTask false
+        }
+
+        val command = if (enable) "enable" else "disable"
+        executeShellCommand("cmd overlay $command $packageName")
+        executor.execute(task)
+        assertThat(task.get(TIMEOUT, TimeUnit.SECONDS)).isTrue()
+    }
+
+    private fun executeShellCommand(command: String): String {
+        val uiAutomation = instrumentation.uiAutomation
+        val pfd = uiAutomation.executeShellCommand(command)
+        return ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.reader().readText() }
+    }
+}
+
+class TestActivity : Activity() {
+
+    var callback: Callback? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(FrameLayout(this).apply {
+            setBackgroundColor(Color.BLUE)
+        })
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        callback?.onConfigurationChanged(newConfig)
+    }
+
+    interface Callback {
+        fun onConfigurationChanged(newConfig: Configuration)
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt
new file mode 100644
index 0000000..09fd27e
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.res.Resources
+import android.content.res.loader.ResourceLoader
+import android.content.res.loader.ResourcesProvider
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import com.google.common.truth.Truth.assertThat
+import org.hamcrest.CoreMatchers.not
+import org.junit.Assume.assumeThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(Parameterized::class)
+class ResourceLoaderDrawableTest : ResourceLoaderTestBase() {
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = arrayOf(DataType.APK, DataType.ARSC)
+    }
+
+    @field:Parameterized.Parameter(0)
+    override lateinit var dataType: DataType
+
+    @Test
+    fun matchingConfig() {
+        val original = getDrawable(android.R.drawable.ic_delete)
+        val loader = "drawableMdpiWithoutFile".openLoader()
+        `when`(loader.first.loadDrawable(any(), anyInt(), anyInt(), any()))
+                .thenReturn(ColorDrawable(Color.BLUE))
+
+        addLoader(loader)
+
+        updateConfiguration { densityDpi = 160 /* mdpi */ }
+
+        val drawable = getDrawable(android.R.drawable.ic_delete)
+
+        loader.verifyLoadDrawableCalled()
+
+        assertThat(drawable).isNotEqualTo(original)
+        assertThat(drawable).isInstanceOf(ColorDrawable::class.java)
+        assertThat((drawable as ColorDrawable).color).isEqualTo(Color.BLUE)
+    }
+
+    @Test
+    fun worseConfig() {
+        val loader = "drawableMdpiWithoutFile".openLoader()
+        addLoader(loader)
+
+        updateConfiguration { densityDpi = 480 /* xhdpi */ }
+
+        getDrawable(android.R.drawable.ic_delete)
+
+        verify(loader.first, never()).loadDrawable(any(), anyInt(), anyInt(), any())
+    }
+
+    @Test
+    fun multipleLoaders() {
+        val original = getDrawable(android.R.drawable.ic_delete)
+        val loaderOne = "drawableMdpiWithoutFile".openLoader()
+        val loaderTwo = "drawableMdpiWithoutFile".openLoader()
+
+        `when`(loaderTwo.first.loadDrawable(any(), anyInt(), anyInt(), any()))
+                .thenReturn(ColorDrawable(Color.BLUE))
+
+        addLoader(loaderOne, loaderTwo)
+
+        updateConfiguration { densityDpi = 160 /* mdpi */ }
+
+        val drawable = getDrawable(android.R.drawable.ic_delete)
+        loaderOne.verifyLoadDrawableNotCalled()
+        loaderTwo.verifyLoadDrawableCalled()
+
+        assertThat(drawable).isNotEqualTo(original)
+        assertThat(drawable).isInstanceOf(ColorDrawable::class.java)
+        assertThat((drawable as ColorDrawable).color).isEqualTo(Color.BLUE)
+    }
+
+    @Test(expected = Resources.NotFoundException::class)
+    fun multipleLoadersNoReturnWithoutFile() {
+        val loaderOne = "drawableMdpiWithoutFile".openLoader()
+        val loaderTwo = "drawableMdpiWithoutFile".openLoader()
+
+        addLoader(loaderOne, loaderTwo)
+
+        updateConfiguration { densityDpi = 160 /* mdpi */ }
+
+        try {
+            getDrawable(android.R.drawable.ic_delete)
+        } finally {
+            // We expect the call to fail because at least the loader won't resolve the overridden
+            // drawable, but we should still verify that both loaders were called before allowing
+            // the exception to propagate.
+            loaderOne.verifyLoadDrawableNotCalled()
+            loaderTwo.verifyLoadDrawableCalled()
+        }
+    }
+
+    @Test
+    fun multipleLoadersReturnWithFile() {
+        // Can't return a file if an ARSC
+        assumeThat(dataType, not(DataType.ARSC))
+
+        val original = getDrawable(android.R.drawable.ic_delete)
+        val loaderOne = "drawableMdpiWithFile".openLoader()
+        val loaderTwo = "drawableMdpiWithFile".openLoader()
+
+        addLoader(loaderOne, loaderTwo)
+
+        updateConfiguration { densityDpi = 160 /* mdpi */ }
+
+        val drawable = getDrawable(android.R.drawable.ic_delete)
+        loaderOne.verifyLoadDrawableNotCalled()
+        loaderTwo.verifyLoadDrawableCalled()
+
+        assertThat(drawable).isNotNull()
+        assertThat(drawable).isInstanceOf(original.javaClass)
+    }
+
+    @Test
+    fun unhandledResourceIgnoresLoaders() {
+        val loader = "drawableMdpiWithoutFile".openLoader()
+        `when`(loader.first.loadDrawable(any(), anyInt(), anyInt(), any()))
+                .thenReturn(ColorDrawable(Color.BLUE))
+        addLoader(loader)
+
+        getDrawable(android.R.drawable.ic_menu_add)
+
+        loader.verifyLoadDrawableNotCalled()
+
+        getDrawable(android.R.drawable.ic_delete)
+
+        loader.verifyLoadDrawableCalled()
+    }
+
+    private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadDrawableCalled() {
+        verify(first).loadDrawable(
+                argThat {
+                    it.density == 160 &&
+                            it.resourceId == android.R.drawable.ic_delete &&
+                            it.string == "res/drawable-mdpi-v4/ic_delete.png"
+                },
+                eq(android.R.drawable.ic_delete),
+                eq(0),
+                any()
+        )
+    }
+
+    private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadDrawableNotCalled() {
+        verify(first, never()).loadDrawable(
+                any(),
+                anyInt(),
+                anyInt(),
+                any()
+        )
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt
new file mode 100644
index 0000000..1ec2094
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.res.Resources
+import android.content.res.XmlResourceParser
+import android.content.res.loader.ResourceLoader
+import android.content.res.loader.ResourcesProvider
+import com.google.common.truth.Truth.assertThat
+import org.hamcrest.CoreMatchers.not
+import org.junit.Assume.assumeThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(Parameterized::class)
+class ResourceLoaderLayoutTest : ResourceLoaderTestBase() {
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = arrayOf(DataType.APK, DataType.ARSC)
+    }
+
+    @field:Parameterized.Parameter(0)
+    override lateinit var dataType: DataType
+
+    @Test
+    fun singleLoader() {
+        val original = getLayout(android.R.layout.activity_list_item)
+        val mockXml = mock(XmlResourceParser::class.java)
+        val loader = "layoutWithoutFile".openLoader()
+        `when`(loader.first.loadXmlResourceParser(any(), anyInt()))
+                .thenReturn(mockXml)
+
+        addLoader(loader)
+
+        val layout = getLayout(android.R.layout.activity_list_item)
+        loader.verifyLoadLayoutCalled()
+
+        assertThat(layout).isNotEqualTo(original)
+        assertThat(layout).isSameAs(mockXml)
+    }
+
+    @Test
+    fun multipleLoaders() {
+        val original = getLayout(android.R.layout.activity_list_item)
+        val loaderOne = "layoutWithoutFile".openLoader()
+        val loaderTwo = "layoutWithoutFile".openLoader()
+
+        val mockXml = mock(XmlResourceParser::class.java)
+        `when`(loaderTwo.first.loadXmlResourceParser(any(), anyInt()))
+                .thenReturn(mockXml)
+
+        addLoader(loaderOne, loaderTwo)
+
+        val layout = getLayout(android.R.layout.activity_list_item)
+        loaderOne.verifyLoadLayoutNotCalled()
+        loaderTwo.verifyLoadLayoutCalled()
+
+        assertThat(layout).isNotEqualTo(original)
+        assertThat(layout).isSameAs(mockXml)
+    }
+
+    @Test(expected = Resources.NotFoundException::class)
+    fun multipleLoadersNoReturnWithoutFile() {
+        val loaderOne = "layoutWithoutFile".openLoader()
+        val loaderTwo = "layoutWithoutFile".openLoader()
+
+        addLoader(loaderOne, loaderTwo)
+
+        try {
+            getLayout(android.R.layout.activity_list_item)
+        } finally {
+            // We expect the call to fail because at least one loader must resolve the overridden
+            // layout, but we should still verify that both loaders were called before allowing
+            // the exception to propagate.
+            loaderOne.verifyLoadLayoutNotCalled()
+            loaderTwo.verifyLoadLayoutCalled()
+        }
+    }
+
+    @Test
+    fun multipleLoadersReturnWithFile() {
+        // Can't return a file if an ARSC
+        assumeThat(dataType, not(DataType.ARSC))
+
+        val loaderOne = "layoutWithFile".openLoader()
+        val loaderTwo = "layoutWithFile".openLoader()
+
+        addLoader(loaderOne, loaderTwo)
+
+        val xml = getLayout(android.R.layout.activity_list_item)
+        loaderOne.verifyLoadLayoutNotCalled()
+        loaderTwo.verifyLoadLayoutCalled()
+
+        assertThat(xml).isNotNull()
+    }
+
+    @Test
+    fun unhandledResourceIgnoresLoaders() {
+        val loader = "layoutWithoutFile".openLoader()
+        val mockXml = mock(XmlResourceParser::class.java)
+        `when`(loader.first.loadXmlResourceParser(any(), anyInt()))
+                .thenReturn(mockXml)
+        addLoader(loader)
+
+        getLayout(android.R.layout.preference_category)
+
+        verify(loader.first, never())
+                .loadXmlResourceParser(anyString(), anyInt())
+
+        getLayout(android.R.layout.activity_list_item)
+
+        loader.verifyLoadLayoutCalled()
+    }
+
+    private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadLayoutCalled() {
+        verify(first).loadXmlResourceParser(
+                "res/layout/activity_list_item.xml",
+                        android.R.layout.activity_list_item
+        )
+    }
+
+    private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadLayoutNotCalled() {
+        verify(first, never()).loadXmlResourceParser(
+                anyString(),
+                anyInt()
+        )
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt
new file mode 100644
index 0000000..5af453d
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.content.res.loader.ResourceLoader
+import android.content.res.loader.ResourcesProvider
+import android.os.ParcelFileDescriptor
+import android.util.TypedValue
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.test.InstrumentationRegistry
+import org.junit.After
+import org.junit.Before
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import java.io.Closeable
+
+abstract class ResourceLoaderTestBase {
+
+    open lateinit var dataType: DataType
+
+    protected lateinit var context: Context
+    protected open val resources: Resources
+        get() = context.resources
+    protected open val assets: AssetManager
+        get() = resources.assets
+
+    // Track opened streams and ResourcesProviders to close them after testing
+    private val openedObjects = mutableListOf<Closeable>()
+
+    @Before
+    fun setUpBase() {
+        context = InstrumentationRegistry.getTargetContext()
+    }
+
+    @After
+    fun removeAllLoaders() {
+        resources.setLoaders(null)
+        openedObjects.forEach {
+            try {
+                it.close()
+            } catch (ignored: Exception) {
+            }
+        }
+    }
+
+    protected fun getString(@StringRes stringRes: Int, debugLog: Boolean = false) =
+            logResolution(debugLog) { getString(stringRes) }
+
+    protected fun getDrawable(@DrawableRes drawableRes: Int, debugLog: Boolean = false) =
+            logResolution(debugLog) { getDrawable(drawableRes) }
+
+    protected fun getLayout(@LayoutRes layoutRes: Int, debugLog: Boolean = false) =
+            logResolution(debugLog) { getLayout(layoutRes) }
+
+    protected fun getDimensionPixelSize(@DimenRes dimenRes: Int, debugLog: Boolean = false) =
+            logResolution(debugLog) { getDimensionPixelSize(dimenRes) }
+
+    private fun <T> logResolution(debugLog: Boolean = false, block: Resources.() -> T): T {
+        if (debugLog) {
+            resources.assets.setResourceResolutionLoggingEnabled(true)
+        }
+
+        var thrown = false
+
+        try {
+            return resources.block()
+        } catch (t: Throwable) {
+            // No good way to log to test output other than throwing an exception
+            if (debugLog) {
+                thrown = true
+                throw IllegalStateException(resources.assets.lastResourceResolution, t)
+            } else {
+                throw t
+            }
+        } finally {
+            if (!thrown && debugLog) {
+                throw IllegalStateException(resources.assets.lastResourceResolution)
+            }
+        }
+    }
+
+    protected fun updateConfiguration(block: Configuration.() -> Unit) {
+        val configuration = Configuration().apply {
+            setTo(resources.configuration)
+            block()
+        }
+
+        resources.updateConfiguration(configuration, resources.displayMetrics)
+    }
+
+    protected fun String.openLoader(
+        dataType: DataType = this@ResourceLoaderTestBase.dataType
+    ): Pair<ResourceLoader, ResourcesProvider> = when (dataType) {
+        DataType.APK -> {
+            mock(ResourceLoader::class.java) to context.copiedRawFile("${this}Apk").use {
+                ResourcesProvider.loadFromApk(it)
+            }.also { openedObjects += it }
+        }
+        DataType.ARSC -> {
+            mock(ResourceLoader::class.java) to openArsc(this)
+        }
+        DataType.SPLIT -> {
+            mock(ResourceLoader::class.java) to ResourcesProvider.loadFromSplit(context, this)
+        }
+        DataType.ASSET -> mockLoader {
+            doAnswer { byteInputStream() }.`when`(it)
+                    .loadAsset(eq("assets/Asset.txt"), anyInt())
+        }
+        DataType.ASSET_FD -> mockLoader {
+            doAnswer {
+                val file = context.filesDir.resolve("Asset.txt")
+                file.writeText(this)
+                ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+            }.`when`(it).loadAssetFd("assets/Asset.txt")
+        }
+        DataType.NON_ASSET -> mockLoader {
+            doAnswer {
+                val file = context.filesDir.resolve("NonAsset.txt")
+                file.writeText(this)
+                ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+            }.`when`(it).loadAssetFd("NonAsset.txt")
+        }
+        DataType.NON_ASSET_DRAWABLE -> mockLoader(openArsc(this)) {
+            doReturn(null).`when`(it).loadDrawable(argThat { value ->
+                value.type == TypedValue.TYPE_STRING &&
+                        value.resourceId == 0x7f010001 &&
+                        value.string == "res/drawable-nodpi-v4/non_asset_drawable.xml"
+            }, eq(0x7f010001), anyInt(), ArgumentMatchers.any())
+
+            doAnswer { context.copiedRawFile(this) }.`when`(it)
+                    .loadAssetFd("res/drawable-nodpi-v4/non_asset_drawable.xml")
+        }
+        DataType.NON_ASSET_BITMAP -> mockLoader(openArsc(this)) {
+            doReturn(null).`when`(it).loadDrawable(argThat { value ->
+                value.type == TypedValue.TYPE_STRING &&
+                        value.resourceId == 0x7f010000 &&
+                        value.string == "res/drawable-nodpi-v4/non_asset_bitmap.png"
+            }, eq(0x7f010000), anyInt(), ArgumentMatchers.any())
+
+            doAnswer { resources.openRawResourceFd(rawFile(this)).createInputStream() }
+                    .`when`(it)
+                    .loadAsset(eq("res/drawable-nodpi-v4/non_asset_bitmap.png"), anyInt())
+        }
+        DataType.NON_ASSET_LAYOUT -> mockLoader(openArsc(this)) {
+            doReturn(null).`when`(it)
+                    .loadXmlResourceParser("res/layout/layout.xml", 0x7f020000)
+
+            doAnswer { context.copiedRawFile(this) }.`when`(it)
+                    .loadAssetFd("res/layout/layout.xml")
+        }
+    }
+
+    protected fun mockLoader(
+        provider: ResourcesProvider = ResourcesProvider.empty(),
+        block: (ResourceLoader) -> Unit = {}
+    ): Pair<ResourceLoader, ResourcesProvider> {
+        return mock(ResourceLoader::class.java, Utils.ANSWER_THROWS)
+                .apply(block) to provider
+    }
+
+    protected fun openArsc(rawName: String): ResourcesProvider {
+        return context.copiedRawFile("${rawName}Arsc")
+                .use { ResourcesProvider.loadFromArsc(it) }
+                .also { openedObjects += it }
+    }
+
+    // This specifically uses addLoader so both behaviors are tested
+    protected fun addLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
+        pairs.forEach { resources.addLoader(it.first, it.second) }
+    }
+
+    protected fun setLoaders(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
+        resources.setLoaders(pairs.map { android.util.Pair(it.first, it.second) })
+    }
+
+    protected fun addLoader(pair: Pair<out ResourceLoader, ResourcesProvider>, index: Int) {
+        resources.addLoader(pair.first, pair.second, index)
+    }
+
+    protected fun removeLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) {
+        pairs.forEach { resources.removeLoader(it.first) }
+    }
+
+    protected fun getLoaders(): MutableList<Pair<ResourceLoader, ResourcesProvider>> {
+        // Cast instead of toMutableList to maintain the same object
+        return resources.getLoaders() as MutableList<Pair<ResourceLoader, ResourcesProvider>>
+    }
+
+    enum class DataType {
+        APK,
+        ARSC,
+        SPLIT,
+        ASSET,
+        ASSET_FD,
+        NON_ASSET,
+        NON_ASSET_DRAWABLE,
+        NON_ASSET_BITMAP,
+        NON_ASSET_LAYOUT,
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt
new file mode 100644
index 0000000..017552a
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Tests generic ResourceLoader behavior. Intentionally abstract in its test methodology because
+ * the behavior being verified isn't specific to any resource type. As long as it can pass an
+ * equals check.
+ *
+ * Currently tests strings and dimens since String and any Number seemed most relevant to verify.
+ */
+@RunWith(Parameterized::class)
+class ResourceLoaderValuesTest : ResourceLoaderTestBase() {
+
+    companion object {
+        @Parameterized.Parameters(name = "{1} {0}")
+        @JvmStatic
+        fun parameters(): Array<Any> {
+            val parameters = mutableListOf<Parameter>()
+
+            // R.string
+            parameters += Parameter(
+                    { getString(android.R.string.cancel) },
+                    "stringOne", { "SomeRidiculouslyUnlikelyStringOne" },
+                    "stringTwo", { "SomeRidiculouslyUnlikelyStringTwo" },
+                    listOf(DataType.APK, DataType.ARSC)
+            )
+
+            // R.dimen
+            parameters += Parameter(
+                    { resources.getDimensionPixelSize(android.R.dimen.app_icon_size) },
+                    "dimenOne", { 564716.dpToPx(resources) },
+                    "dimenTwo", { 565717.dpToPx(resources) },
+                    listOf(DataType.APK, DataType.ARSC)
+            )
+
+            // File in the assets directory
+            parameters += Parameter(
+                    { assets.open("Asset.txt").reader().readText() },
+                    "assetOne", { "assetOne" },
+                    "assetTwo", { "assetTwo" },
+                    listOf(DataType.ASSET)
+            )
+
+            // From assets directory returning file descriptor
+            parameters += Parameter(
+                    { assets.openFd("Asset.txt").readText() },
+                    "assetOne", { "assetOne" },
+                    "assetTwo", { "assetTwo" },
+                    listOf(DataType.ASSET_FD)
+            )
+
+            // From root directory returning file descriptor
+            parameters += Parameter(
+                    { assets.openNonAssetFd("NonAsset.txt").readText() },
+                    "NonAssetOne", { "NonAssetOne" },
+                    "NonAssetTwo", { "NonAssetTwo" },
+                    listOf(DataType.NON_ASSET)
+            )
+
+            // Asset as compiled XML drawable
+            parameters += Parameter(
+                    { (getDrawable(R.drawable.non_asset_drawable) as ColorDrawable).color },
+                    "nonAssetDrawableOne", { Color.parseColor("#A3C3E3") },
+                    "nonAssetDrawableTwo", { Color.parseColor("#3A3C3E") },
+                    listOf(DataType.NON_ASSET_DRAWABLE)
+            )
+
+            // Asset as compiled bitmap drawable
+            parameters += Parameter(
+                    {
+                        (getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable)
+                                .bitmap.getColor(0, 0).toArgb()
+                    },
+                    "nonAssetBitmapGreen", { Color.GREEN },
+                    "nonAssetBitmapBlue", { Color.BLUE },
+                    listOf(DataType.NON_ASSET_BITMAP)
+            )
+
+            // Asset as compiled XML layout
+            parameters += Parameter(
+                    { getLayout(R.layout.layout).advanceToRoot().name },
+                    "layoutOne", { "RelativeLayout" },
+                    "layoutTwo", { "LinearLayout" },
+                    listOf(DataType.NON_ASSET_LAYOUT)
+            )
+
+            // Isolated resource split
+            parameters += Parameter(
+                    { getString(R.string.split_overlaid) },
+                    "split_one", { "Split ONE Overlaid" },
+                    "split_two", { "Split TWO Overlaid" },
+                    listOf(DataType.SPLIT)
+            )
+
+            return parameters.flatMap { parameter ->
+                parameter.dataTypes.map { dataType ->
+                    arrayOf(dataType, parameter)
+                }
+            }.toTypedArray()
+        }
+    }
+
+    @Suppress("LateinitVarOverridesLateinitVar")
+    @field:Parameterized.Parameter(0)
+    override lateinit var dataType: DataType
+
+    @field:Parameterized.Parameter(1)
+    lateinit var parameter: Parameter
+
+    private val valueOne by lazy { parameter.valueOne(this) }
+    private val valueTwo by lazy { parameter.valueTwo(this) }
+
+    private fun openOne() = parameter.loaderOne.openLoader()
+    private fun openTwo() = parameter.loaderTwo.openLoader()
+
+    // Class method for syntax highlighting purposes
+    private fun getValue() = parameter.getValue(this)
+
+    @Test
+    fun verifyValueUniqueness() {
+        // Ensure the parameters are valid in case of coding errors
+        assertNotEquals(valueOne, getValue())
+        assertNotEquals(valueTwo, getValue())
+        assertNotEquals(valueOne, valueTwo)
+    }
+
+    @Test
+    fun addMultipleLoaders() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne, testTwo)
+
+        assertEquals(valueTwo, getValue())
+
+        removeLoader(testTwo)
+
+        assertEquals(valueOne, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(originalValue, getValue())
+    }
+
+    @Test
+    fun setMultipleLoaders() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        setLoaders(testOne, testTwo)
+
+        assertEquals(valueTwo, getValue())
+
+        removeLoader(testTwo)
+
+        assertEquals(valueOne, getValue())
+
+        setLoaders()
+
+        assertEquals(originalValue, getValue())
+    }
+
+    @Test
+    fun getLoadersContainsAll() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne, testTwo)
+
+        assertThat(getLoaders()).containsAllOf(testOne, testTwo)
+    }
+
+    @Test
+    fun getLoadersDoesNotLeakMutability() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        val loaders = getLoaders()
+        loaders += testTwo
+
+        assertEquals(valueOne, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(originalValue, getValue())
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun alreadyAddedThrows() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+        addLoader(testTwo)
+        addLoader(testOne)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun alreadyAddedAndSetThrows() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+        addLoader(testTwo)
+        setLoaders(testTwo)
+    }
+
+    @Test
+    fun repeatedRemoveSucceeds() {
+        val originalValue = getValue()
+        val testOne = openOne()
+
+        addLoader(testOne)
+
+        assertNotEquals(originalValue, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(originalValue, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(originalValue, getValue())
+    }
+
+    @Test
+    fun addToFront() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        addLoader(testTwo, 0)
+
+        assertEquals(valueOne, getValue())
+
+        // Remove top loader, so previously added to front should now resolve
+        removeLoader(testOne)
+        assertEquals(valueTwo, getValue())
+    }
+
+    @Test
+    fun addToEnd() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        addLoader(testTwo, 1)
+
+        assertEquals(valueTwo, getValue())
+    }
+
+    @Test(expected = IndexOutOfBoundsException::class)
+    fun addPastEnd() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        addLoader(testTwo, 2)
+    }
+
+    @Test(expected = IndexOutOfBoundsException::class)
+    fun addBeforeFront() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        addLoader(testTwo, -1)
+    }
+
+    @Test
+    fun reorder() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+
+        addLoader(testOne, testTwo)
+
+        assertEquals(valueTwo, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(valueTwo, getValue())
+
+        addLoader(testOne)
+
+        assertEquals(valueOne, getValue())
+
+        removeLoader(testTwo)
+
+        assertEquals(valueOne, getValue())
+
+        removeLoader(testOne)
+
+        assertEquals(originalValue, getValue())
+    }
+
+    data class Parameter(
+        val getValue: ResourceLoaderValuesTest.() -> Any,
+        val loaderOne: String,
+        val valueOne: ResourceLoaderValuesTest.() -> Any,
+        val loaderTwo: String,
+        val valueTwo: ResourceLoaderValuesTest.() -> Any,
+        val dataTypes: List<DataType>
+    ) {
+        override fun toString(): String {
+            val prefix = loaderOne.commonPrefixWith(loaderTwo)
+            return "$prefix${loaderOne.removePrefix(prefix)}|${loaderTwo.removePrefix(prefix)}"
+        }
+    }
+}
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt
new file mode 100644
index 0000000..df2d09a
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.loader.test
+
+import android.content.Context
+import android.content.res.AssetFileDescriptor
+import android.content.res.Resources
+import android.os.ParcelFileDescriptor
+import android.util.TypedValue
+import org.mockito.Answers
+import org.mockito.stubbing.Answer
+import org.xmlpull.v1.XmlPullParser
+import java.io.File
+
+// Enforce use of [android.util.Pair] instead of Kotlin's so it matches the ResourceLoader APIs
+typealias Pair<F, S> = android.util.Pair<F, S>
+infix fun <A, B> A.to(that: B): Pair<A, B> = Pair.create(this, that)!!
+
+object Utils {
+    val ANSWER_THROWS = Answer<Any> {
+        when (val name = it.method.name) {
+            "toString" -> return@Answer Answers.CALLS_REAL_METHODS.answer(it)
+            else -> throw UnsupportedOperationException("$name with " +
+                    "${it.arguments?.joinToString()} should not be called")
+        }
+    }
+}
+
+fun Int.dpToPx(resources: Resources) = TypedValue.applyDimension(
+        TypedValue.COMPLEX_UNIT_DIP,
+        this.toFloat(),
+        resources.displayMetrics
+).toInt()
+
+fun AssetFileDescriptor.readText() = createInputStream().reader().readText()
+
+fun rawFile(fileName: String) = R.raw::class.java.getDeclaredField(fileName).getInt(null)
+
+fun XmlPullParser.advanceToRoot() = apply {
+    while (next() != XmlPullParser.START_TAG) {
+        // Empty
+    }
+}
+
+fun Context.copiedRawFile(fileName: String): ParcelFileDescriptor {
+    return resources.openRawResourceFd(rawFile(fileName)).use { asset ->
+        // AssetManager doesn't expose a direct file descriptor to the asset, so copy it to
+        // an individual file so one can be created manually.
+        val copiedFile = File(filesDir, fileName)
+        asset.createInputStream().use { input ->
+            copiedFile.outputStream().use { output ->
+                input.copyTo(output)
+            }
+        }
+
+        ParcelFileDescriptor.open(copiedFile, ParcelFileDescriptor.MODE_READ_WRITE)
+    }
+}
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index eeaefc5..a34a6c0 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -166,7 +166,10 @@
             static_libs: common_test_libs + ["liblog", "libz"],
         },
     },
-    data: ["tests/data/**/*.apk"],
+    data: [
+      "tests/data/**/*.apk",
+      "tests/data/**/*.arsc",
+    ],
     test_suites: ["device-tests"],
 }
 
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index cf2ef30..b309621 100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -42,12 +42,16 @@
 
 ApkAssets::ApkAssets(ZipArchiveHandle unmanaged_handle,
                      const std::string& path,
-                     time_t last_mod_time)
-    : zip_handle_(unmanaged_handle, ::CloseArchive), path_(path), last_mod_time_(last_mod_time) {
+                     time_t last_mod_time,
+                     bool for_loader)
+    : zip_handle_(unmanaged_handle, ::CloseArchive), path_(path), last_mod_time_(last_mod_time),
+      for_loader(for_loader) {
 }
 
-std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) {
-  return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/);
+std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system,
+                                                 bool for_loader) {
+  return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/,
+                  for_loader);
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadAsSharedLibrary(const std::string& path,
@@ -76,9 +80,21 @@
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadFromFd(unique_fd fd,
                                                        const std::string& friendly_name,
-                                                       bool system, bool force_shared_lib) {
+                                                       bool system, bool force_shared_lib,
+                                                       bool for_loader) {
   return LoadImpl(std::move(fd), friendly_name, nullptr /*idmap_asset*/, nullptr /*loaded_idmap*/,
-                  system, force_shared_lib);
+                  system, force_shared_lib, for_loader);
+}
+
+std::unique_ptr<const ApkAssets> ApkAssets::LoadArsc(const std::string& path,
+                                                     bool for_loader) {
+  return LoadArscImpl({} /*fd*/, path, for_loader);
+}
+
+std::unique_ptr<const ApkAssets> ApkAssets::LoadArsc(unique_fd fd,
+                                                     const std::string& friendly_name,
+                                                     bool for_loader) {
+  return LoadArscImpl(std::move(fd), friendly_name, for_loader);
 }
 
 std::unique_ptr<Asset> ApkAssets::CreateAssetFromFile(const std::string& path) {
@@ -104,7 +120,8 @@
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(
     unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset,
-    std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) {
+    std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library,
+    bool for_loader) {
   ::ZipArchiveHandle unmanaged_handle;
   int32_t result;
   if (fd >= 0) {
@@ -123,7 +140,8 @@
   time_t last_mod_time = getFileModDate(path.c_str());
 
   // Wrap the handle in a unique_ptr so it gets automatically closed.
-  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time));
+  std::unique_ptr<ApkAssets>
+      loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time, for_loader));
 
   // Find the resource table.
   ::ZipEntry entry;
@@ -152,7 +170,7 @@
       reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),
       loaded_apk->resources_asset_->getLength());
   loaded_apk->loaded_arsc_ =
-      LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
+      LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library, for_loader);
   if (loaded_apk->loaded_arsc_ == nullptr) {
     LOG(ERROR) << "Failed to load '" << kResourcesArsc << "' in APK '" << path << "'.";
     return {};
@@ -162,8 +180,53 @@
   return std::move(loaded_apk);
 }
 
+std::unique_ptr<const ApkAssets> ApkAssets::LoadArscImpl(unique_fd fd,
+                                                         const std::string& path,
+                                                         bool for_loader) {
+  std::unique_ptr<Asset> resources_asset;
+
+  if (fd >= 0) {
+    resources_asset = std::unique_ptr<Asset>(Asset::createFromFd(fd.release(), nullptr,
+        Asset::AccessMode::ACCESS_BUFFER));
+  } else {
+    resources_asset = CreateAssetFromFile(path);
+  }
+
+  if (resources_asset == nullptr) {
+    LOG(ERROR) << "Failed to open ARSC '" << path;
+    return {};
+  }
+
+  time_t last_mod_time = getFileModDate(path.c_str());
+
+  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(nullptr, path, last_mod_time, for_loader));
+  loaded_apk->resources_asset_ = std::move(resources_asset);
+
+  const StringPiece data(
+      reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),
+      loaded_apk->resources_asset_->getLength());
+  loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, nullptr, false, false, for_loader);
+  if (loaded_apk->loaded_arsc_ == nullptr) {
+    LOG(ERROR) << "Failed to load '" << kResourcesArsc << path;
+    return {};
+  }
+
+  // Need to force a move for mingw32.
+  return std::move(loaded_apk);
+}
+
+std::unique_ptr<const ApkAssets> ApkAssets::LoadEmpty(bool for_loader) {
+  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(nullptr, "", -1, for_loader));
+  loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
+  // Need to force a move for mingw32.
+  return std::move(loaded_apk);
+}
+
 std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMode mode) const {
-  CHECK(zip_handle_ != nullptr);
+  // If this is a resource loader from an .arsc, there will be no zip handle
+  if (zip_handle_ == nullptr) {
+    return {};
+  }
 
   ::ZipEntry entry;
   int32_t result = ::FindEntry(zip_handle_.get(), path, &entry);
@@ -205,7 +268,10 @@
 
 bool ApkAssets::ForEachFile(const std::string& root_path,
                             const std::function<void(const StringPiece&, FileType)>& f) const {
-  CHECK(zip_handle_ != nullptr);
+  // If this is a resource loader from an .arsc, there will be no zip handle
+  if (zip_handle_ == nullptr) {
+    return false;
+  }
 
   std::string root_path_full = root_path;
   if (root_path_full.back() != '/') {
@@ -252,6 +318,11 @@
 }
 
 bool ApkAssets::IsUpToDate() const {
+  // Loaders are invalidated by the app, not the system, so assume up to date
+  if (for_loader) {
+    return true;
+  }
+
   return last_mod_time_ == getFileModDate(path_.c_str());
 }
 
diff --git a/libs/androidfw/Asset.cpp b/libs/androidfw/Asset.cpp
index 92125c9..c132f34 100644
--- a/libs/androidfw/Asset.cpp
+++ b/libs/androidfw/Asset.cpp
@@ -133,14 +133,24 @@
  */
 /*static*/ Asset* Asset::createFromFile(const char* fileName, AccessMode mode)
 {
+    return createFromFd(open(fileName, O_RDONLY | O_BINARY), fileName, mode);
+}
+
+/*
+ * Create a new Asset from a file on disk.  There is a fair chance that
+ * the file doesn't actually exist.
+ *
+ * We can use "mode" to decide how we want to go about it.
+ */
+/*static*/ Asset* Asset::createFromFd(const int fd, const char* fileName, AccessMode mode)
+{
+    if (fd < 0) {
+        return NULL;
+    }
+
     _FileAsset* pAsset;
     status_t result;
     off64_t length;
-    int fd;
-
-    fd = open(fileName, O_RDONLY | O_BINARY);
-    if (fd < 0)
-        return NULL;
 
     /*
      * Under Linux, the lseek fails if we actually opened a directory.  To
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index eec49df..e914f37 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -493,8 +493,12 @@
 
     type_flags |= type_spec->GetFlagsForEntryIndex(local_entry_idx);
 
-    // If the package is an overlay, then even configurations that are the same MUST be chosen.
+
+    // If the package is an overlay or custom loader,
+    // then even configurations that are the same MUST be chosen.
     const bool package_is_overlay = loaded_package->IsOverlay();
+    const bool package_is_loader = loaded_package->IsCustomLoader();
+    const bool should_overlay = package_is_overlay || package_is_loader;
 
     if (use_fast_path) {
       const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
@@ -508,10 +512,28 @@
         if (best_config == nullptr) {
           resolution_type = Resolution::Step::Type::INITIAL;
         } else if (this_config.isBetterThan(*best_config, desired_config)) {
-          resolution_type = Resolution::Step::Type::BETTER_MATCH;
-        } else if (package_is_overlay && this_config.compare(*best_config) == 0) {
-          resolution_type = Resolution::Step::Type::OVERLAID;
+          if (package_is_loader) {
+            resolution_type = Resolution::Step::Type::BETTER_MATCH_LOADER;
+          } else {
+            resolution_type = Resolution::Step::Type::BETTER_MATCH;
+          }
+        } else if (should_overlay && this_config.compare(*best_config) == 0) {
+          if (package_is_loader) {
+            resolution_type = Resolution::Step::Type::OVERLAID_LOADER;
+          } else if (package_is_overlay) {
+            resolution_type = Resolution::Step::Type::OVERLAID;
+          }
         } else {
+          if (resource_resolution_logging_enabled_) {
+            if (package_is_loader) {
+              resolution_type = Resolution::Step::Type::SKIPPED_LOADER;
+            } else {
+              resolution_type = Resolution::Step::Type::SKIPPED;
+            }
+            resolution_steps.push_back(Resolution::Step{resolution_type,
+                                                        this_config.toString(),
+                                                        &loaded_package->GetPackageName()});
+          }
           continue;
         }
 
@@ -520,6 +542,16 @@
         const ResTable_type* type = filtered_group.types[i];
         const uint32_t offset = LoadedPackage::GetEntryOffset(type, local_entry_idx);
         if (offset == ResTable_type::NO_ENTRY) {
+          if (resource_resolution_logging_enabled_) {
+            if (package_is_loader) {
+              resolution_type = Resolution::Step::Type::NO_ENTRY_LOADER;
+            } else {
+              resolution_type = Resolution::Step::Type::NO_ENTRY;
+            }
+            resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::NO_ENTRY,
+                                                        this_config.toString(),
+                                                        &loaded_package->GetPackageName()});
+          }
           continue;
         }
 
@@ -554,9 +586,17 @@
           if (best_config == nullptr) {
             resolution_type = Resolution::Step::Type::INITIAL;
           } else if (this_config.isBetterThan(*best_config, desired_config)) {
-            resolution_type = Resolution::Step::Type::BETTER_MATCH;
-          } else if (package_is_overlay && this_config.compare(*best_config) == 0) {
-            resolution_type = Resolution::Step::Type::OVERLAID;
+            if (package_is_loader) {
+              resolution_type = Resolution::Step::Type::BETTER_MATCH_LOADER;
+            } else {
+              resolution_type = Resolution::Step::Type::BETTER_MATCH;
+            }
+          } else if (should_overlay && this_config.compare(*best_config) == 0) {
+            if (package_is_overlay) {
+              resolution_type = Resolution::Step::Type::OVERLAID;
+            } else if (package_is_loader) {
+              resolution_type = Resolution::Step::Type::OVERLAID_LOADER;
+            }
           } else {
             continue;
           }
@@ -678,9 +718,27 @@
       case Resolution::Step::Type::BETTER_MATCH:
         prefix = "Found better";
         break;
+      case Resolution::Step::Type::BETTER_MATCH_LOADER:
+        prefix = "Found better in loader";
+        break;
       case Resolution::Step::Type::OVERLAID:
         prefix = "Overlaid";
         break;
+      case Resolution::Step::Type::OVERLAID_LOADER:
+        prefix = "Overlaid by loader";
+        break;
+      case Resolution::Step::Type::SKIPPED:
+        prefix = "Skipped";
+        break;
+      case Resolution::Step::Type::SKIPPED_LOADER:
+        prefix = "Skipped loader";
+        break;
+      case Resolution::Step::Type::NO_ENTRY:
+        prefix = "No entry";
+        break;
+      case Resolution::Step::Type::NO_ENTRY_LOADER:
+        prefix = "No entry for loader";
+        break;
     }
 
     if (!prefix.empty()) {
diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp
index 72873ab..882dc0d 100644
--- a/libs/androidfw/LoadedArsc.cpp
+++ b/libs/androidfw/LoadedArsc.cpp
@@ -401,7 +401,9 @@
 
 std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
                                                          const LoadedIdmap* loaded_idmap,
-                                                         bool system, bool load_as_shared_library) {
+                                                         bool system,
+                                                         bool load_as_shared_library,
+                                                         bool for_loader) {
   ATRACE_NAME("LoadedPackage::Load");
   std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage());
 
@@ -430,6 +432,10 @@
     loaded_package->overlay_ = true;
   }
 
+  if (for_loader) {
+    loaded_package->custom_loader_ = true;
+  }
+
   if (header->header.headerSize >= sizeof(ResTable_package)) {
     uint32_t type_id_offset = dtohl(header->typeIdOffset);
     if (type_id_offset > std::numeric_limits<uint8_t>::max()) {
@@ -696,7 +702,7 @@
 }
 
 bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
-                           bool load_as_shared_library) {
+                           bool load_as_shared_library, bool for_loader) {
   const ResTable_header* header = chunk.header<ResTable_header>();
   if (header == nullptr) {
     LOG(ERROR) << "RES_TABLE_TYPE too small.";
@@ -735,7 +741,11 @@
         packages_seen++;
 
         std::unique_ptr<const LoadedPackage> loaded_package =
-            LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
+            LoadedPackage::Load(child_chunk,
+                                loaded_idmap,
+                                system_,
+                                load_as_shared_library,
+                                for_loader);
         if (!loaded_package) {
           return false;
         }
@@ -758,9 +768,11 @@
 }
 
 std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
-                                                   const LoadedIdmap* loaded_idmap, bool system,
-                                                   bool load_as_shared_library) {
-  ATRACE_NAME("LoadedArsc::LoadTable");
+                                                   const LoadedIdmap* loaded_idmap,
+                                                   bool system,
+                                                   bool load_as_shared_library,
+                                                   bool for_loader) {
+  ATRACE_NAME("LoadedArsc::Load");
 
   // Not using make_unique because the constructor is private.
   std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
@@ -771,7 +783,10 @@
     const Chunk chunk = iter.Next();
     switch (chunk.type()) {
       case RES_TABLE_TYPE:
-        if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
+        if (!loaded_arsc->LoadTable(chunk,
+                                    loaded_idmap,
+                                    load_as_shared_library,
+                                    for_loader)) {
           return {};
         }
         break;
diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h
index 49fc82b..625b6820 100644
--- a/libs/androidfw/include/androidfw/ApkAssets.h
+++ b/libs/androidfw/include/androidfw/ApkAssets.h
@@ -40,7 +40,8 @@
   // Creates an ApkAssets.
   // If `system` is true, the package is marked as a system package, and allows some functions to
   // filter out this package when computing what configurations/resources are available.
-  static std::unique_ptr<const ApkAssets> Load(const std::string& path, bool system = false);
+  static std::unique_ptr<const ApkAssets> Load(const std::string& path, bool system = false,
+                                               bool for_loader = false);
 
   // Creates an ApkAssets, but forces any package with ID 0x7f to be loaded as a shared library.
   // If `system` is true, the package is marked as a system package, and allows some functions to
@@ -63,7 +64,21 @@
   // If `force_shared_lib` is true, any package with ID 0x7f is loaded as a shared library.
   static std::unique_ptr<const ApkAssets> LoadFromFd(base::unique_fd fd,
                                                      const std::string& friendly_name, bool system,
-                                                     bool force_shared_lib);
+                                                     bool force_shared_lib,
+                                                     bool for_loader = false);
+
+  // Creates an empty wrapper ApkAssets from the given path which points to an .arsc.
+  static std::unique_ptr<const ApkAssets> LoadArsc(const std::string& path,
+                                                   bool for_loader = false);
+
+  // Creates an empty wrapper ApkAssets from the given file descriptor which points to an .arsc,
+  // Takes ownership of the file descriptor.
+  static std::unique_ptr<const ApkAssets> LoadArsc(base::unique_fd fd,
+                                                   const std::string& friendly_name,
+                                                   bool resource_loader = false);
+
+  // Creates a totally empty ApkAssets with no resources table and no file entries.
+  static std::unique_ptr<const ApkAssets> LoadEmpty(bool resource_loader = false);
 
   std::unique_ptr<Asset> Open(const std::string& path,
                               Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM) const;
@@ -86,24 +101,33 @@
 
   bool IsUpToDate() const;
 
+  // Creates an Asset from any file on the file system.
+  static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path);
+
  private:
   DISALLOW_COPY_AND_ASSIGN(ApkAssets);
 
   static std::unique_ptr<const ApkAssets> LoadImpl(base::unique_fd fd, const std::string& path,
                                                    std::unique_ptr<Asset> idmap_asset,
                                                    std::unique_ptr<const LoadedIdmap> loaded_idmap,
-                                                   bool system, bool load_as_shared_library);
+                                                   bool system, bool load_as_shared_library,
+                                                   bool resource_loader = false);
 
-  // Creates an Asset from any file on the file system.
-  static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path);
+  static std::unique_ptr<const ApkAssets> LoadArscImpl(base::unique_fd fd,
+                                                       const std::string& path,
+                                                       bool resource_loader = false);
 
-  ApkAssets(ZipArchiveHandle unmanaged_handle, const std::string& path, time_t last_mod_time);
+  ApkAssets(ZipArchiveHandle unmanaged_handle,
+            const std::string& path,
+            time_t last_mod_time,
+            bool for_loader = false);
 
-  using ZipArchivePtr = std::unique_ptr<ZipArchive, void(*)(ZipArchiveHandle)>;
+  using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>;
 
   ZipArchivePtr zip_handle_;
   const std::string path_;
   time_t last_mod_time_;
+  bool for_loader;
   std::unique_ptr<Asset> resources_asset_;
   std::unique_ptr<Asset> idmap_asset_;
   std::unique_ptr<const LoadedArsc> loaded_arsc_;
diff --git a/libs/androidfw/include/androidfw/Asset.h b/libs/androidfw/include/androidfw/Asset.h
index 9d12a35..053dbb7 100644
--- a/libs/androidfw/include/androidfw/Asset.h
+++ b/libs/androidfw/include/androidfw/Asset.h
@@ -121,6 +121,11 @@
      */
     const char* getAssetSource(void) const { return mAssetSource.string(); }
 
+    /*
+     * Create the asset from a file descriptor.
+     */
+    static Asset* createFromFd(const int fd, const char* fileName, AccessMode mode);
+
 protected:
     /*
      * Adds this Asset to the global Asset list for debugging and
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index de46081..c7348b1 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -382,7 +382,13 @@
       enum class Type {
         INITIAL,
         BETTER_MATCH,
-        OVERLAID
+        BETTER_MATCH_LOADER,
+        OVERLAID,
+        OVERLAID_LOADER,
+        SKIPPED,
+        SKIPPED_LOADER,
+        NO_ENTRY,
+        NO_ENTRY_LOADER,
       };
 
       // Marks what kind of override this step was.
diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h
index 950f541..1a56876 100644
--- a/libs/androidfw/include/androidfw/LoadedArsc.h
+++ b/libs/androidfw/include/androidfw/LoadedArsc.h
@@ -137,7 +137,8 @@
 
   static std::unique_ptr<const LoadedPackage> Load(const Chunk& chunk,
                                                    const LoadedIdmap* loaded_idmap, bool system,
-                                                   bool load_as_shared_library);
+                                                   bool load_as_shared_library,
+                                                   bool load_as_custom_loader);
 
   ~LoadedPackage();
 
@@ -187,6 +188,11 @@
     return overlay_;
   }
 
+  // Returns true if this package is a custom loader and should behave like an overlay
+  inline bool IsCustomLoader() const {
+    return custom_loader_;
+  }
+
   // Returns the map of package name to package ID used in this LoadedPackage. At runtime, a
   // package could have been assigned a different package ID than what this LoadedPackage was
   // compiled with. AssetManager rewrites the package IDs so that they are compatible at runtime.
@@ -260,6 +266,7 @@
   bool dynamic_ = false;
   bool system_ = false;
   bool overlay_ = false;
+  bool custom_loader_ = false;
   bool defines_overlayable_ = false;
 
   ByteBucketArray<TypeSpecPtr> type_specs_;
@@ -282,7 +289,8 @@
   static std::unique_ptr<const LoadedArsc> Load(const StringPiece& data,
                                                 const LoadedIdmap* loaded_idmap = nullptr,
                                                 bool system = false,
-                                                bool load_as_shared_library = false);
+                                                bool load_as_shared_library = false,
+                                                bool for_loader = false);
 
   // Create an empty LoadedArsc. This is used when an APK has no resources.arsc.
   static std::unique_ptr<const LoadedArsc> CreateEmpty();
@@ -311,7 +319,19 @@
   DISALLOW_COPY_AND_ASSIGN(LoadedArsc);
 
   LoadedArsc() = default;
-  bool LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool load_as_shared_library);
+  bool LoadTable(
+      const Chunk& chunk,
+      const LoadedIdmap* loaded_idmap,
+      bool load_as_shared_library,
+      bool for_loader
+  );
+
+  static std::unique_ptr<const LoadedArsc> LoadData(std::unique_ptr<LoadedArsc>& loaded_arsc,
+                                                    const char* data,
+                                                    size_t length,
+                                                    const LoadedIdmap* loaded_idmap = nullptr,
+                                                    bool load_as_shared_library = false,
+                                                    bool for_loader = false);
 
   ResStringPool global_string_pool_;
   std::vector<std::unique_ptr<const LoadedPackage>> packages_;
diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp
index d58e8d2..fd57a92 100644
--- a/libs/androidfw/tests/LoadedArsc_test.cpp
+++ b/libs/androidfw/tests/LoadedArsc_test.cpp
@@ -25,6 +25,7 @@
 #include "data/overlayable/R.h"
 #include "data/sparse/R.h"
 #include "data/styles/R.h"
+#include "data/system/R.h"
 
 namespace app = com::android::app;
 namespace basic = com::android::basic;
@@ -387,6 +388,39 @@
   ASSERT_EQ(map.at("OverlayableResources2"), "overlay://com.android.overlayable");
 }
 
+TEST(LoadedArscTest, LoadCustomLoader) {
+  std::string contents;
+
+  std::unique_ptr<Asset>
+      asset = ApkAssets::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc");
+
+  MockLoadedIdmap loaded_idmap;
+  const StringPiece data(
+      reinterpret_cast<const char*>(asset->getBuffer(true /*wordAligned*/)),
+      asset->getLength());
+
+  std::unique_ptr<const LoadedArsc> loaded_arsc =
+      LoadedArsc::Load(data, nullptr, false, false, true);
+  ASSERT_THAT(loaded_arsc, NotNull());
+
+  const LoadedPackage* package =
+      loaded_arsc->GetPackageById(get_package_id(android::R::string::cancel));
+  ASSERT_THAT(package, NotNull());
+  EXPECT_THAT(package->GetPackageName(), StrEq("android"));
+  EXPECT_THAT(package->GetPackageId(), Eq(0x01));
+
+  const uint8_t type_index = get_type_id(android::R::string::cancel) - 1;
+  const uint16_t entry_index = get_entry_id(android::R::string::cancel);
+
+  const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+  ASSERT_THAT(type_spec, NotNull());
+  ASSERT_THAT(type_spec->type_count, Ge(1u));
+
+  const ResTable_type* type = type_spec->types[0];
+  ASSERT_THAT(type, NotNull());
+  ASSERT_THAT(LoadedPackage::GetEntry(type, entry_index), NotNull());
+}
+
 // structs with size fields (like Res_value, ResTable_entry) should be
 // backwards and forwards compatible (aka checking the size field against
 // sizeof(Res_value) might not be backwards compatible.
diff --git a/libs/androidfw/tests/data/loader/resources.arsc b/libs/androidfw/tests/data/loader/resources.arsc
new file mode 100644
index 0000000..2c881f2
--- /dev/null
+++ b/libs/androidfw/tests/data/loader/resources.arsc
Binary files differ
diff --git a/libs/androidfw/tests/data/system/R.h b/libs/androidfw/tests/data/system/R.h
index becb388..3741074 100644
--- a/libs/androidfw/tests/data/system/R.h
+++ b/libs/androidfw/tests/data/system/R.h
@@ -40,6 +40,12 @@
       number = 0x01030000,  // sv
     };
   };
+
+  struct string {
+    enum : uint32_t {
+      cancel = 0x01040000,
+    };
+  };
 };
 
 }  // namespace android