Merge changes from topic "res-loader"

* changes:
  Refactor ResourcesLoader Tests
  Refactor ResourcesLoader APIs
diff --git a/apct-tests/perftests/core/src/android/app/ResourcesManagerPerfTest.java b/apct-tests/perftests/core/src/android/app/ResourcesManagerPerfTest.java
index 2955d2c..050fecd 100644
--- a/apct-tests/perftests/core/src/android/app/ResourcesManagerPerfTest.java
+++ b/apct-tests/perftests/core/src/android/app/ResourcesManagerPerfTest.java
@@ -80,7 +80,7 @@
     private void getResourcesForPath(String path) {
         ResourcesManager.getInstance().getResources(null, path, null, null, null,
                 Display.DEFAULT_DISPLAY, null, sContext.getResources().getCompatibilityInfo(),
-                null);
+                null, null);
     }
 
     @Test
diff --git a/apct-tests/perftests/core/src/android/app/ResourcesThemePerfTest.java b/apct-tests/perftests/core/src/android/app/ResourcesThemePerfTest.java
index 6123e69..f4c0a17 100644
--- a/apct-tests/perftests/core/src/android/app/ResourcesThemePerfTest.java
+++ b/apct-tests/perftests/core/src/android/app/ResourcesThemePerfTest.java
@@ -96,7 +96,7 @@
 
         Resources destResources = resourcesManager.getResources(null, ai.sourceDir,
                 ai.splitSourceDirs, ai.resourceDirs, ai.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
-                c, mContext.getResources().getCompatibilityInfo(), null);
+                c, mContext.getResources().getCompatibilityInfo(), null, null);
         Assert.assertNotEquals(destResources.getAssets(), mContext.getAssets());
 
         Resources.Theme destTheme = destResources.newTheme();
diff --git a/api/current.txt b/api/current.txt
index 12d8b70..77dec70 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -12653,8 +12653,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 void addLoader(@NonNull android.content.res.loader.ResourcesLoader);
+    method public void clearLoaders();
     method public final void finishPreloading();
     method public final void flushLayoutCache();
     method @NonNull public android.content.res.XmlResourceParser getAnimation(@AnimRes @AnimatorRes int) throws android.content.res.Resources.NotFoundException;
@@ -12681,7 +12681,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 @NonNull public java.util.List<android.content.res.loader.ResourcesLoader> 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;
@@ -12709,8 +12709,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 public void removeLoader(@NonNull android.content.res.loader.ResourcesLoader);
+    method public void setLoaders(@NonNull java.util.List<android.content.res.loader.ResourcesLoader>);
     method @Deprecated public void updateConfiguration(android.content.res.Configuration, android.util.DisplayMetrics);
     field @AnyRes public static final int ID_NULL = 0; // 0x0
   }
@@ -12780,27 +12780,37 @@
 
 package android.content.res.loader {
 
-  public class DirectoryResourceLoader implements android.content.res.loader.ResourceLoader {
-    ctor public DirectoryResourceLoader(@NonNull java.io.File);
+  public interface AssetsProvider {
+    method @Nullable public default java.io.InputStream loadAsset(@NonNull String, int) throws java.io.IOException;
+    method @Nullable public default android.os.ParcelFileDescriptor loadAssetParcelFd(@NonNull String) throws java.io.IOException;
+  }
+
+  public class DirectoryAssetsProvider implements android.content.res.loader.AssetsProvider {
+    ctor public DirectoryAssetsProvider(@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 class ResourcesLoader {
+    ctor public ResourcesLoader();
+    method public void addProvider(@NonNull android.content.res.loader.ResourcesProvider);
+    method public void clearProviders();
+    method @NonNull public java.util.List<android.content.res.loader.ResourcesProvider> getProviders();
+    method public void removeProvider(@NonNull android.content.res.loader.ResourcesProvider);
+    method public void setProviders(@NonNull java.util.List<android.content.res.loader.ResourcesProvider>);
   }
 
-  public final class ResourcesProvider implements java.lang.AutoCloseable java.io.Closeable {
+  public 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 empty(@NonNull android.content.res.loader.AssetsProvider);
+    method @Nullable public android.content.res.loader.AssetsProvider getAssetsProvider();
     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.ParcelFileDescriptor, @Nullable android.content.res.loader.AssetsProvider) 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 loadFromApk(@NonNull android.os.SharedMemory, @Nullable android.content.res.loader.AssetsProvider) throws java.io.IOException;
     method @NonNull public static android.content.res.loader.ResourcesProvider loadFromSplit(@NonNull android.content.Context, @NonNull String) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromTable(@NonNull android.os.ParcelFileDescriptor, @Nullable android.content.res.loader.AssetsProvider) throws java.io.IOException;
+    method @NonNull public static android.content.res.loader.ResourcesProvider loadFromTable(@NonNull android.os.SharedMemory, @Nullable android.content.res.loader.AssetsProvider) throws java.io.IOException;
   }
 
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index d90e81f..c901d2a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2198,7 +2198,7 @@
     Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
             String[] libDirs, int displayId, LoadedApk pkgInfo) {
         return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
-                displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
+                displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader(), null);
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index b7555ee..136c84e 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -46,6 +46,7 @@
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.loader.ResourcesLoader;
 import android.database.DatabaseErrorHandler;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteDatabase.CursorFactory;
@@ -100,6 +101,7 @@
 import java.nio.ByteOrder;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -2217,7 +2219,8 @@
     }
 
     private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
-            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
+            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo,
+            List<ResourcesLoader> resourcesLoader) {
         final String[] splitResDirs;
         final ClassLoader classLoader;
         try {
@@ -2234,7 +2237,8 @@
                 displayId,
                 overrideConfig,
                 compatInfo,
-                classLoader);
+                classLoader,
+                resourcesLoader);
     }
 
     @Override
@@ -2249,7 +2253,7 @@
             final int displayId = getDisplayId();
 
             c.setResources(createResources(mToken, pi, null, displayId, null,
-                    getDisplayAdjustments(displayId).getCompatibilityInfo()));
+                    getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
             if (c.mResources != null) {
                 return c;
             }
@@ -2284,7 +2288,7 @@
             final int displayId = getDisplayId();
 
             c.setResources(createResources(mToken, pi, null, displayId, null,
-                    getDisplayAdjustments(displayId).getCompatibilityInfo()));
+                    getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
             if (c.mResources != null) {
                 return c;
             }
@@ -2328,7 +2332,8 @@
                 displayId,
                 null,
                 mPackageInfo.getCompatibilityInfo(),
-                classLoader));
+                classLoader,
+                mResources.getLoaders()));
         return context;
     }
 
@@ -2342,8 +2347,10 @@
                 mSplitName, mToken, mUser, mFlags, mClassLoader, null);
 
         final int displayId = getDisplayId();
+
         context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId,
-                overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo()));
+                overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo(),
+                mResources.getLoaders()));
         return context;
     }
 
@@ -2357,8 +2364,10 @@
                 mSplitName, mToken, mUser, mFlags, mClassLoader, null);
 
         final int displayId = display.getDisplayId();
+
         context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId,
-                null, getDisplayAdjustments(displayId).getCompatibilityInfo()));
+                null, getDisplayAdjustments(displayId).getCompatibilityInfo(),
+                mResources.getLoaders()));
         context.mDisplay = display;
         return context;
     }
@@ -2564,7 +2573,7 @@
         ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo, null,
                 null, null, null, 0, null, null);
         context.setResources(createResources(null, packageInfo, null, displayId, null,
-                packageInfo.getCompatibilityInfo()));
+                packageInfo.getCompatibilityInfo(), null));
         context.updateDisplay(displayId);
         context.mIsSystemOrSystemUiContext = true;
         return context;
@@ -2637,7 +2646,8 @@
                 displayId,
                 overrideConfiguration,
                 compatInfo,
-                classLoader));
+                classLoader,
+                packageInfo.getApplication().getResources().getLoaders()));
         context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                 context.getResources());
         return context;
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index f0d0e98..44c2486 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -365,7 +365,8 @@
                 mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                         splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                         Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
-                        getClassLoader());
+                        getClassLoader(), mApplication == null ? null
+                                : mApplication.getResources().getLoaders());
             }
         }
         mAppComponentFactory = createAppFactory(aInfo, mDefaultClassLoader);
@@ -1158,7 +1159,7 @@
             mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                     splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                     Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
-                    getClassLoader());
+                    getClassLoader(), null);
         }
         return mResources;
     }
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 7ab85a4..d09f0bc 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -32,7 +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.content.res.loader.ResourcesLoader;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.IBinder;
 import android.os.Process;
@@ -46,7 +46,6 @@
 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;
@@ -57,9 +56,7 @@
 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;
@@ -98,52 +95,6 @@
             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
@@ -219,6 +170,11 @@
     private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>>
             mAdjustedDisplays = new ArrayMap<>();
 
+    /**
+     * Callback implementation for handling updates to Resources objects.
+     */
+    private final UpdateHandler mUpdateCallbacks = new UpdateHandler();
+
     @UnsupportedAppUsage
     public ResourcesManager() {
     }
@@ -253,24 +209,6 @@
                 }
             }
 
-            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--) {
@@ -513,6 +451,12 @@
             }
         }
 
+        if (key.mLoaders != null) {
+            for (final ResourcesLoader loader : key.mLoaders) {
+                builder.addLoader(loader);
+            }
+        }
+
         return builder.build();
     }
 
@@ -570,16 +514,6 @@
 
             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);
         }
     }
 
@@ -660,19 +594,6 @@
      */
     private @Nullable ResourcesKey findKeyForResourceImplLocked(
             @NonNull ResourcesImpl resourceImpl) {
-        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);
@@ -722,30 +643,10 @@
     @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();
+        final int size = activityResources.activityResources.size();
         for (int index = 0; index < size; index++) {
             WeakReference<Resources> ref = activityResources.activityResources.get(index);
             Resources resources = ref.get();
@@ -771,6 +672,7 @@
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
+        resources.setCallbacks(mUpdateCallbacks);
         activityResources.activityResources.add(new WeakReference<>(resources));
         if (DEBUG) {
             Slog.d(TAG, "- creating new ref=" + resources);
@@ -784,6 +686,7 @@
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
+        resources.setCallbacks(mUpdateCallbacks);
         mResourceReferences.add(new WeakReference<>(resources));
         if (DEBUG) {
             Slog.d(TAG, "- creating new ref=" + resources);
@@ -795,7 +698,7 @@
     /**
      * Creates base resources for an Activity. Calls to
      * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration,
-     * CompatibilityInfo, ClassLoader)} with the same activityToken will have their override
+     * CompatibilityInfo, ClassLoader, List)} with the same activityToken will have their override
      * configurations merged with the one specified here.
      *
      * @param activityToken Represents an Activity.
@@ -820,7 +723,8 @@
             int displayId,
             @Nullable Configuration overrideConfig,
             @NonNull CompatibilityInfo compatInfo,
-            @Nullable ClassLoader classLoader) {
+            @Nullable ClassLoader classLoader,
+            @Nullable List<ResourcesLoader> loaders) {
         try {
             Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
                     "ResourcesManager#createBaseActivityResources");
@@ -831,7 +735,8 @@
                     libDirs,
                     displayId,
                     overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
-                    compatInfo);
+                    compatInfo,
+                    loaders == null ? null : loaders.toArray(new ResourcesLoader[0]));
             classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
 
             if (DEBUG) {
@@ -902,14 +807,6 @@
             } 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);
-                }
-            }
         }
     }
 
@@ -983,7 +880,8 @@
             int displayId,
             @Nullable Configuration overrideConfig,
             @NonNull CompatibilityInfo compatInfo,
-            @Nullable ClassLoader classLoader) {
+            @Nullable ClassLoader classLoader,
+            @Nullable List<ResourcesLoader> loaders) {
         try {
             Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
             final ResourcesKey key = new ResourcesKey(
@@ -993,7 +891,8 @@
                     libDirs,
                     displayId,
                     overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
-                    compatInfo);
+                    compatInfo,
+                    loaders == null ? null : loaders.toArray(new ResourcesLoader[0]));
             classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
 
             cleanupReferences(activityToken);
@@ -1010,10 +909,10 @@
 
     /**
      * Updates an Activity's Resources object with overrideConfig. The Resources object
-     * that was previously returned by
-     * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration,
-     * CompatibilityInfo, ClassLoader)} is
-     * still valid and will have the updated configuration.
+     * that was previously returned by {@link #getResources(IBinder, String, String[], String[],
+     * String[], int, Configuration, CompatibilityInfo, ClassLoader, List)} is still valid and will
+     * have the updated configuration.
+     *
      * @param activityToken The Activity token.
      * @param overrideConfig The configuration override to update.
      * @param displayId Id of the display where activity currently resides.
@@ -1074,25 +973,6 @@
                             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;
-                    }
-
-                    ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig,
-                            overrideConfig, displayId);
-
-                    updateActivityResources(resources, newKey, true);
-
-                    resourcesWithLoaders.updateKey(newKey);
-                }
             }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
@@ -1133,9 +1013,8 @@
 
         // 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);
+                oldKey.mSplitResDirs, oldKey.mOverlayDirs, oldKey.mLibDirs,
+                displayId, rebasedOverrideConfig, oldKey.mCompatInfo, oldKey.mLoaders);
 
         if (DEBUG) {
             Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey
@@ -1214,18 +1093,6 @@
                 }
             }
 
-            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,
-                        resourcesWithLoaders.resourcesKey(), resources.getImpl());
-            }
-
             return changes != 0;
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
@@ -1306,37 +1173,8 @@
                                 newLibAssets,
                                 key.mDisplayId,
                                 key.mOverrideConfiguration,
-                                key.mCompatInfo));
-                    }
-                }
-            }
-
-            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));
+                                key.mCompatInfo,
+                                key.mLoaders));
                     }
                 }
             }
@@ -1345,72 +1183,6 @@
         }
     }
 
-    /**
-     * 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) {
@@ -1450,37 +1222,12 @@
                             key.mLibDirs,
                             key.mDisplayId,
                             key.mOverrideConfiguration,
-                            key.mCompatInfo
+                            key.mCompatInfo,
+                            key.mLoaders
                     ));
                 }
             }
 
-            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);
@@ -1530,25 +1277,62 @@
                 }
             }
         }
+    }
 
-        // 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;
+    private class UpdateHandler implements Resources.UpdateCallbacks {
+
+        /**
+         * Updates the list of {@link ResourcesLoader ResourcesLoader(s)} that the {@code resources}
+         * instance uses.
+         */
+        @Override
+        public void onLoadersChanged(Resources resources, List<ResourcesLoader> newLoader) {
+            synchronized (ResourcesManager.this) {
+                final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
+                if (oldKey == null) {
+                    throw new IllegalArgumentException("Cannot modify resource loaders of"
+                            + " ResourcesImpl not registered with ResourcesManager");
+                }
+
+                final ResourcesKey newKey = new ResourcesKey(
+                        oldKey.mResDir,
+                        oldKey.mSplitResDirs,
+                        oldKey.mOverlayDirs,
+                        oldKey.mLibDirs,
+                        oldKey.mDisplayId,
+                        oldKey.mOverrideConfiguration,
+                        oldKey.mCompatInfo,
+                        newLoader.toArray(new ResourcesLoader[0]));
+
+                final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(newKey);
+                resources.setImpl(impl);
             }
+        }
 
-            ResourcesKey newKey = updatedResourceKeys.get(resources.getImpl());
-            if (newKey == null) {
-                continue;
+        /**
+         * Refreshes the {@link AssetManager} of all {@link ResourcesImpl} that contain the
+         * {@code loader} to apply any changes of the set of {@link ApkAssets}.
+         **/
+        @Override
+        public void onLoaderUpdated(ResourcesLoader loader) {
+            synchronized (ResourcesManager.this) {
+                final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceImplKeys =
+                        new ArrayMap<>();
+
+                for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
+                    final ResourcesKey key = mResourceImpls.keyAt(i);
+                    final WeakReference<ResourcesImpl> impl = mResourceImpls.valueAt(i);
+                    if (impl == null || impl.get() == null
+                            || !ArrayUtils.contains(key.mLoaders, loader)) {
+                        continue;
+                    }
+
+                    mResourceImpls.remove(key);
+                    updatedResourceImplKeys.put(impl.get(), key);
+                }
+
+                redirectResourcesToNewImplLocked(updatedResourceImplKeys);
             }
-
-            resourcesWithLoaders.updateKey(newKey);
-            resourcesWithLoaders.resources()
-                    .setImpl(createResourcesImpl(newKey));
         }
     }
 }
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index dbfc650..430241a 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -8521,7 +8521,8 @@
                 Display.DEFAULT_DISPLAY,
                 null,
                 systemResources.getCompatibilityInfo(),
-                systemResources.getClassLoader());
+                systemResources.getClassLoader(),
+                null);
 
         sUseRoundIcon = overlayableRes.getBoolean(com.android.internal.R.bool.config_useRoundIcon);
     }
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 96fbe91..1b01758 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -27,13 +27,12 @@
 import android.compat.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.AssetsProvider;
+import android.content.res.loader.ResourcesLoader;
 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;
 
@@ -47,6 +46,7 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -113,12 +113,7 @@
     @GuardedBy("this") private int mNumRefs = 1;
     @GuardedBy("this") private HashMap<Long, RuntimeException> mRefStacks;
 
-    private ResourceLoaderManager mResourceLoaderManager;
-
-    /** @hide */
-    public void setResourceLoaderManager(ResourceLoaderManager resourceLoaderManager) {
-        mResourceLoaderManager = resourceLoaderManager;
-    }
+    private ResourcesLoader[] mLoaders;
 
     /**
      * A Builder class that helps create an AssetManager with only a single invocation of
@@ -130,32 +125,66 @@
      */
     public static class Builder {
         private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>();
+        private ArrayList<ResourcesLoader> mLoaders = new ArrayList<>();
 
         public Builder addApkAssets(ApkAssets apkAssets) {
             mUserApkAssets.add(apkAssets);
             return this;
         }
 
+        public Builder addLoader(ResourcesLoader loader) {
+            mLoaders.add(loader);
+            return this;
+        }
+
         public AssetManager build() {
             // Retrieving the system ApkAssets forces their creation as well.
             final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
 
-            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
+            // Filter ApkAssets so that assets provided by multiple loaders are only included once
+            // in the AssetManager assets. The last appearance of the ApkAssets dictates its load
+            // order.
+            final ArrayList<ApkAssets> loaderApkAssets = new ArrayList<>();
+            final ArraySet<ApkAssets> uniqueLoaderApkAssets = new ArraySet<>();
+            for (int i = mLoaders.size() - 1; i >= 0; i--) {
+                final List<ApkAssets> currentLoaderApkAssets = mLoaders.get(i).getApkAssets();
+                for (int j = currentLoaderApkAssets.size() - 1; j >= 0; j--) {
+                    final ApkAssets apkAssets = currentLoaderApkAssets.get(j);
+                    if (uniqueLoaderApkAssets.contains(apkAssets)) {
+                        continue;
+                    }
+
+                    uniqueLoaderApkAssets.add(apkAssets);
+                    loaderApkAssets.add(0, apkAssets);
+                }
+            }
+
+            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size()
+                    + loaderApkAssets.size();
             final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
 
             System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
 
-            final int userApkAssetCount = mUserApkAssets.size();
-            for (int i = 0; i < userApkAssetCount; i++) {
+            // Append user ApkAssets after system ApkAssets.
+            for (int i = 0, n = mUserApkAssets.size(); i < n; i++) {
                 apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
             }
 
+            // Append ApkAssets provided by loaders to the end.
+            for (int i = 0, n = loaderApkAssets.size(); i < n; i++) {
+                apkAssets[i + systemApkAssets.length  + mUserApkAssets.size()] =
+                        loaderApkAssets.get(i);
+            }
+
             // Calling this constructor prevents creation of system ApkAssets, which we took care
             // of in this Builder.
             final AssetManager assetManager = new AssetManager(false /*sentinel*/);
             assetManager.mApkAssets = apkAssets;
             AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                     false /*invalidateCaches*/);
+            assetManager.mLoaders = mLoaders.isEmpty() ? null
+                    : mLoaders.toArray(new ResourcesLoader[0]);
+
             return assetManager;
         }
     }
@@ -432,6 +461,12 @@
         }
     }
 
+    /** @hide */
+    @NonNull
+    public List<ResourcesLoader> getLoaders() {
+        return mLoaders == null ? Collections.emptyList() : Arrays.asList(mLoaders);
+    }
+
     /**
      * Ensures that the native implementation has not been destroyed.
      * The AssetManager may have been closed, but references to it still exist
@@ -1056,38 +1091,70 @@
         }
     }
 
-    private InputStream searchLoaders(int cookie, @NonNull String fileName, int accessMode)
-            throws IOException {
-        if (mResourceLoaderManager == null) {
+    private ResourcesProvider findResourcesProvider(int assetCookie) {
+        if (mLoaders == null) {
             return null;
         }
 
-        List<Pair<ResourceLoader, ResourcesProvider>> loaders =
-                mResourceLoaderManager.getInternalList();
+        int apkAssetsIndex = assetCookie - 1;
+        if (apkAssetsIndex >= mApkAssets.length || apkAssetsIndex < 0) {
+            return null;
+        }
 
-        // A cookie of 0 means no specific ApkAssets, so search everything
+        final ApkAssets apkAssets = mApkAssets[apkAssetsIndex];
+        if (!apkAssets.isForLoader()) {
+            return null;
+        }
+
+        for (int i = mLoaders.length - 1; i >= 0; i--) {
+            final ResourcesLoader loader = mLoaders[i];
+            for (int j = 0, n = loader.getProviders().size(); j < n; j++) {
+                final ResourcesProvider provider = loader.getProviders().get(j);
+                if (apkAssets == provider.getApkAssets()) {
+                    return provider;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private InputStream searchLoaders(int cookie, @NonNull String fileName, int accessMode)
+            throws IOException {
+        if (mLoaders == null) {
+            return null;
+        }
+
         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;
+            // A cookie of 0 means no specific ApkAssets, so search everything
+            for (int i = mLoaders.length - 1; i >= 0; i--) {
+                final ResourcesLoader loader = mLoaders[i];
+                final List<ResourcesProvider> providers = loader.getProviders();
+                for (int j = providers.size() - 1; j >= 0; j--) {
+                    final AssetsProvider assetsProvider = providers.get(j).getAssetsProvider();
+                    if (assetsProvider == null) {
+                        continue;
                     }
-                } catch (IOException ignored) {
-                    // When searching, ignore read failures
+
+                    try {
+                        final InputStream inputStream = assetsProvider.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);
-            }
+        final ResourcesProvider provider = findResourcesProvider(cookie);
+        if (provider != null && provider.getAssetsProvider() != null) {
+            return provider.getAssetsProvider().loadAsset(
+                    fileName, accessMode);
         }
 
         return null;
@@ -1095,43 +1162,48 @@
 
     private AssetFileDescriptor searchLoadersFd(int cookie, @NonNull String fileName)
             throws IOException {
-        if (mResourceLoaderManager == null) {
+        if (mLoaders == 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);
+            // A cookie of 0 means no specific ApkAssets, so search everything
+            for (int i = mLoaders.length - 1; i >= 0; i--) {
+                final ResourcesLoader loader = mLoaders[i];
+                final List<ResourcesProvider> providers = loader.getProviders();
+                for (int j = providers.size() - 1; j >= 0; j--) {
+                    final AssetsProvider assetsProvider = providers.get(j).getAssetsProvider();
+                    if (assetsProvider == null) {
+                        continue;
                     }
-                } catch (IOException ignored) {
-                    // When searching, ignore read failures
+
+                    try {
+                        final ParcelFileDescriptor fileDescriptor = assetsProvider
+                                .loadAssetParcelFd(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;
+        final ResourcesProvider provider = findResourcesProvider(cookie);
+        if (provider != null && provider.getAssetsProvider() != null) {
+            final ParcelFileDescriptor fileDescriptor = provider.getAssetsProvider()
+                    .loadAssetParcelFd(fileName);
+            if (fileDescriptor != null) {
+                return new AssetFileDescriptor(fileDescriptor, 0,
+                        AssetFileDescriptor.UNKNOWN_LENGTH);
             }
+            return null;
         }
+
         return null;
     }
 
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 4725e0a..471e83c 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -30,7 +30,6 @@
 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,13 +40,10 @@
 import android.annotation.StyleRes;
 import android.annotation.StyleableRes;
 import android.annotation.XmlRes;
-import android.app.ResourcesManager;
 import android.compat.annotation.UnsupportedAppUsage;
 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.content.res.loader.ResourcesLoader;
 import android.graphics.Movie;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
@@ -55,18 +51,17 @@
 import android.graphics.drawable.DrawableInflater;
 import android.os.Build;
 import android.os.Bundle;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 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;
@@ -117,6 +112,7 @@
     static final String TAG = "Resources";
 
     private static final Object sSync = new Object();
+    private final Object mLock = new Object();
 
     // Used by BridgeResources in layoutlib
     @UnsupportedAppUsage
@@ -143,10 +139,7 @@
     @UnsupportedAppUsage
     final ClassLoader mClassLoader;
 
-    private final Object mResourceLoaderLock = new Object();
-
-    @GuardedBy("mResourceLoaderLock")
-    private ResourceLoaderManager mResourceLoaderManager;
+    private UpdateCallbacks mCallbacks = null;
 
     /**
      * WeakReferences to Themes that were constructed from this Resources object.
@@ -240,6 +233,18 @@
         }
     }
 
+    /** @hide */
+    public interface UpdateCallbacks extends ResourcesLoader.UpdateCallbacks {
+        /**
+         * Invoked when a {@link Resources} instance has a {@link ResourcesLoader} added, removed,
+         * or reordered.
+         *
+         * @param resources the instance being updated
+         * @param newLoaders the new set of loaders for the instance
+         */
+        void onLoadersChanged(Resources resources, List<ResourcesLoader> newLoaders);
+    }
+
     /**
      * Create a new Resources object on top of an existing set of assets in an
      * AssetManager.
@@ -303,12 +308,6 @@
         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();
@@ -322,6 +321,15 @@
         }
     }
 
+    /** @hide */
+    public void setCallbacks(UpdateCallbacks callbacks) {
+        if (mCallbacks != null) {
+            throw new IllegalStateException("callback already registered");
+        }
+
+        mCallbacks = callbacks;
+    }
+
     /**
      * @hide
      */
@@ -937,14 +945,6 @@
     @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);
     }
 
@@ -2337,14 +2337,6 @@
     @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);
     }
 
@@ -2371,136 +2363,106 @@
         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;
-                        }
-                    }
-                }
-            }
+    private void checkCallbacksRegistered() {
+        if (mCallbacks == null) {
+            throw new IllegalArgumentException("Cannot modify resource loaders of Resources"
+                    + " instances created outside of ResourcesManager");
         }
-
-        return null;
     }
 
     /**
-     * @return copied list of loaders and providers previously added
+     * Retrieves the list of loaders.
+     *
+     * <p>Loaders are listed in increasing precedence order. A loader will override the resources
+     * and assets of loaders listed before itself.
      */
     @NonNull
-    public List<Pair<ResourceLoader, ResourcesProvider>> getLoaders() {
-        synchronized (mResourceLoaderLock) {
-            return mResourceLoaderManager == null
-                    ? Collections.emptyList()
-                    : mResourceLoaderManager.getLoaders();
-        }
+    public List<ResourcesLoader> getLoaders() {
+        return mResourcesImpl.getAssets().getLoaders();
     }
 
     /**
-     * Add a custom {@link ResourceLoader} which is added to the paths searched by
-     * {@link AssetManager} when resolving a resource.
+     * Appends a loader to the end of the loader list. If the loader is already present in the
+     * loader list, the list will not be modified.
      *
-     * 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
+     * @param loader the loader to add
      */
-    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);
+    public void addLoader(@NonNull ResourcesLoader loader) {
+        synchronized (mLock) {
+            checkCallbacksRegistered();
+
+            final List<ResourcesLoader> loaders = new ArrayList<>(
+                    mResourcesImpl.getAssets().getLoaders());
+            if (loaders.contains(loader)) {
+                return;
             }
 
-            mResourceLoaderManager.addLoader(resourceLoader, resourcesProvider, index);
+            loaders.add(loader);
+            mCallbacks.onLoadersChanged(this, loaders);
+            loader.registerOnProvidersChangedCallback(this, mCallbacks);
         }
     }
 
     /**
-     * @see #addLoader(ResourceLoader, ResourcesProvider, int).
+     * Removes a loader from the loaders. If the loader is not present in the loader list, the list
+     * will not be modified.
      *
-     * Adds to the end of the list.
-     *
-     * @return index the loader was added at
+     * @param loader the loader to remove
      */
-    public int addLoader(@NonNull ResourceLoader resourceLoader,
-            @NonNull ResourcesProvider resourcesProvider) {
-        synchronized (mResourceLoaderLock) {
-            int index = getLoaders().size();
-            addLoader(resourceLoader, resourcesProvider, index);
-            return index;
-        }
-    }
+    public void removeLoader(@NonNull ResourcesLoader loader) {
+        synchronized (mLock) {
+            checkCallbacksRegistered();
 
-    /**
-     * 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;
+            final List<ResourcesLoader> loaders = new ArrayList<>(
+                    mResourcesImpl.getAssets().getLoaders());
+            if (!loaders.remove(loader)) {
+                return;
             }
 
-            return mResourceLoaderManager.removeLoader(resourceLoader);
+            mCallbacks.onLoadersChanged(this, loaders);
+            loader.unregisterOnProvidersChangedCallback(this);
         }
     }
 
     /**
-     * Swap the current set of loaders. Preferred to multiple remove/add calls as this doesn't
-     * update the resource data structures after each modification.
+     * Sets the list of loaders.
      *
-     * 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
+     * @param loaders the new loaders
      */
-    public void setLoaders(
-            @Nullable List<Pair<ResourceLoader, ResourcesProvider>> resourceLoadersAndProviders) {
-        synchronized (mResourceLoaderLock) {
-            if (mResourceLoaderManager == null) {
-                if (ArrayUtils.isEmpty(resourceLoadersAndProviders)) {
-                    return;
+    public void setLoaders(@NonNull List<ResourcesLoader> loaders) {
+        synchronized (mLock) {
+            checkCallbacksRegistered();
+
+            final List<ResourcesLoader> oldLoaders = mResourcesImpl.getAssets().getLoaders();
+            int index = 0;
+            boolean modified = loaders.size() != oldLoaders.size();
+            final ArraySet<ResourcesLoader> seenLoaders = new ArraySet<>();
+            for (final ResourcesLoader loader : loaders) {
+                if (!seenLoaders.add(loader)) {
+                    throw new IllegalArgumentException("Loader " + loader + " present twice");
                 }
 
-                ResourcesManager.getInstance().registerForLoaders(this);
-                mResourceLoaderManager = new ResourceLoaderManager(mResourcesImpl);
+                if (!modified && oldLoaders.get(index++) != loader) {
+                    modified = true;
+                }
             }
 
-            mResourceLoaderManager.setLoaders(resourceLoadersAndProviders);
+            if (!modified) {
+                return;
+            }
+
+            mCallbacks.onLoadersChanged(this, loaders);
+            for (int i = 0, n = oldLoaders.size(); i < n; i++) {
+                oldLoaders.get(i).unregisterOnProvidersChangedCallback(this);
+            }
+            for (ResourcesLoader newLoader : loaders) {
+                newLoader.registerOnProvidersChangedCallback(this, mCallbacks);
+            }
         }
     }
+
+    /** Removes all {@link ResourcesLoader ResourcesLoader(s)}. */
+    public void clearLoaders() {
+        setLoaders(Collections.emptyList());
+    }
 }
diff --git a/core/java/android/content/res/ResourcesKey.java b/core/java/android/content/res/ResourcesKey.java
index a29fea0..9e40f46 100644
--- a/core/java/android/content/res/ResourcesKey.java
+++ b/core/java/android/content/res/ResourcesKey.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.res.loader.ResourcesLoader;
 import android.text.TextUtils;
 
 import java.util.Arrays;
@@ -48,6 +49,9 @@
     @NonNull
     public final CompatibilityInfo mCompatInfo;
 
+    @Nullable
+    public final ResourcesLoader[] mLoaders;
+
     private final int mHash;
 
     @UnsupportedAppUsage
@@ -57,11 +61,13 @@
                         @Nullable String[] libDirs,
                         int displayId,
                         @Nullable Configuration overrideConfig,
-                        @Nullable CompatibilityInfo compatInfo) {
+                        @Nullable CompatibilityInfo compatInfo,
+                        @Nullable ResourcesLoader[] loader) {
         mResDir = resDir;
         mSplitResDirs = splitResDirs;
         mOverlayDirs = overlayDirs;
         mLibDirs = libDirs;
+        mLoaders = (loader != null && loader.length == 0) ? null : loader;
         mDisplayId = displayId;
         mOverrideConfiguration = new Configuration(overrideConfig != null
                 ? overrideConfig : Configuration.EMPTY);
@@ -75,6 +81,7 @@
         hash = 31 * hash + mDisplayId;
         hash = 31 * hash + Objects.hashCode(mOverrideConfiguration);
         hash = 31 * hash + Objects.hashCode(mCompatInfo);
+        hash = 31 * hash + Arrays.hashCode(mLoaders);
         mHash = hash;
     }
 
@@ -140,6 +147,9 @@
         if (!Objects.equals(mCompatInfo, peer.mCompatInfo)) {
             return false;
         }
+        if (!Arrays.equals(mLoaders, peer.mLoaders)) {
+            return false;
+        }
         return true;
     }
 
@@ -167,7 +177,11 @@
         builder.append(" mOverrideConfig=").append(Configuration.resourceQualifierString(
                 mOverrideConfiguration));
         builder.append(" mCompatInfo=").append(mCompatInfo);
-        builder.append("}");
+        builder.append(" mLoaders=[");
+        if (mLoaders != null) {
+            builder.append(TextUtils.join(",", mLoaders));
+        }
+        builder.append("]}");
         return builder.toString();
     }
 }
diff --git a/core/java/android/content/res/loader/AssetsProvider.java b/core/java/android/content/res/loader/AssetsProvider.java
new file mode 100644
index 0000000..c315494
--- /dev/null
+++ b/core/java/android/content/res/loader/AssetsProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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.content.res.AssetManager;
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Provides callbacks that allow for the value of a file-based resources or assets of a
+ * {@link ResourcesProvider} to be specified or overridden.
+ */
+public interface AssetsProvider {
+
+    /**
+     * Callback that allows the value of a file-based resources or asset to be specified or
+     * overridden.
+     *
+     * <p>There are two situations in which this method will be called:
+     * <ul>
+     * <li>AssetManager is queried for an InputStream of an asset using APIs like
+     * {@link AssetManager#open} and {@link AssetManager#openXmlResourceParser}.
+     * <li>AssetManager is resolving the value of a file-based resource provided by the
+     * {@link ResourcesProvider} this instance is associated with.
+     * </ul>
+     *
+     * <p>If the value retrieved from this callback is null, AssetManager will attempt to find the
+     * file-based resource or asset within the APK provided by the ResourcesProvider this instance
+     * is associated with.
+     *
+     * @param path the asset path being loaded
+     * @param accessMode the {@link AssetManager} access mode
+     *
+     * @see AssetManager#open
+     */
+    @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 being loaded
+     */
+    @Nullable
+    default ParcelFileDescriptor loadAssetParcelFd(@NonNull String path) throws IOException {
+        return null;
+    }
+}
diff --git a/core/java/android/content/res/loader/DirectoryResourceLoader.java b/core/java/android/content/res/loader/DirectoryAssetsProvider.java
similarity index 70%
rename from core/java/android/content/res/loader/DirectoryResourceLoader.java
rename to core/java/android/content/res/loader/DirectoryAssetsProvider.java
index 7d90e72..81c2a4c 100644
--- a/core/java/android/content/res/loader/DirectoryResourceLoader.java
+++ b/core/java/android/content/res/loader/DirectoryAssetsProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -26,23 +26,27 @@
 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.
+ * A {@link AssetsProvider} that searches a directory for assets.
+ * Assumes that resource paths are resolvable child paths of the root directory passed in.
  */
-public class DirectoryResourceLoader implements ResourceLoader {
+public class DirectoryAssetsProvider implements AssetsProvider {
 
     @NonNull
     private final File mDirectory;
 
-    public DirectoryResourceLoader(@NonNull File directory) {
+    /**
+     * Creates a DirectoryAssetsProvider with given root directory.
+     *
+     * @param directory the root directory to resolve files from
+     */
+    public DirectoryAssetsProvider(@NonNull File directory) {
         this.mDirectory = directory;
     }
 
     @Nullable
     @Override
     public InputStream loadAsset(@NonNull String path, int accessMode) throws IOException {
-        File file = findFile(path);
+        final File file = findFile(path);
         if (file == null || !file.exists()) {
             return null;
         }
@@ -51,8 +55,8 @@
 
     @Nullable
     @Override
-    public ParcelFileDescriptor loadAssetFd(@NonNull String path) throws IOException {
-        File file = findFile(path);
+    public ParcelFileDescriptor loadAssetParcelFd(@NonNull String path) throws IOException {
+        final File file = findFile(path);
         if (file == null || !file.exists()) {
             return null;
         }
@@ -60,7 +64,9 @@
     }
 
     /**
-     * Find the file for the given path encoded into the resource table.
+     * Finds the file relative to the root directory.
+     *
+     * @param path the relative path of the file
      */
     @Nullable
     public File findFile(@NonNull String path) {
diff --git a/core/java/android/content/res/loader/ResourceLoader.java b/core/java/android/content/res/loader/ResourceLoader.java
deleted file mode 100644
index af32aa2..0000000
--- a/core/java/android/content/res/loader/ResourceLoader.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 592ec09..0000000
--- a/core/java/android/content/res/loader/ResourceLoaderManager.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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;
-            this.mResourcesImpl.getAssets().setResourceLoaderManager(this);
-            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/ResourcesLoader.java b/core/java/android/content/res/loader/ResourcesLoader.java
new file mode 100644
index 0000000..69dacee
--- /dev/null
+++ b/core/java/android/content/res/loader/ResourcesLoader.java
@@ -0,0 +1,253 @@
+/*
+ * 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.res.ApkAssets;
+import android.content.res.Resources;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A container for supplying {@link ResourcesProvider ResourcesProvider(s)} to {@link Resources}
+ * objects.
+ *
+ * <p>{@link ResourcesLoader ResourcesLoader(s)} are added to Resources objects to supply
+ * additional resources and assets or modify the values of existing resources and assets. Multiple
+ * Resources objects can share the same ResourcesLoaders and ResourcesProviders. Changes to the list
+ * of {@link ResourcesProvider ResourcesProvider(s)} a loader contains propagates to all Resources
+ * objects that use the loader.
+ *
+ * <p>Loaders retrieved with {@link Resources#getLoaders()} are listed in increasing precedence
+ * order. A loader will override the resources and assets of loaders listed before itself.
+ *
+ * <p>Providers retrieved with {@link #getProviders()} are listed in increasing precedence order. A
+ * provider will override the resources and assets of providers listed before itself.
+ */
+public class ResourcesLoader {
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private ApkAssets[] mApkAssets;
+
+    @GuardedBy("mLock")
+    private ResourcesProvider[] mPreviousProviders;
+
+    @GuardedBy("mLock")
+    private ResourcesProvider[] mProviders;
+
+    @GuardedBy("mLock")
+    private ArrayMap<WeakReference<Object>, UpdateCallbacks> mChangeCallbacks = new ArrayMap<>();
+
+    /** @hide */
+    public interface UpdateCallbacks {
+
+        /**
+         * Invoked when a {@link ResourcesLoader} has a {@link ResourcesProvider} added, removed,
+         * or reordered.
+         *
+         * @param loader the loader that was updated
+         */
+        void onLoaderUpdated(@NonNull ResourcesLoader loader);
+    }
+
+    /**
+     * Retrieves the list of providers loaded into this instance. Providers are listed in increasing
+     * precedence order. A provider will override the values of providers listed before itself.
+     */
+    @NonNull
+    public List<ResourcesProvider> getProviders() {
+        synchronized (mLock) {
+            return mProviders == null ? Collections.emptyList() : Arrays.asList(mProviders);
+        }
+    }
+
+    /**
+     * Appends a provider to the end of the provider list. If the provider is already present in the
+     * loader list, the list will not be modified.
+     *
+     * @param resourcesProvider the provider to add
+     */
+    public void addProvider(@NonNull ResourcesProvider resourcesProvider) {
+        synchronized (mLock) {
+            mProviders = ArrayUtils.appendElement(ResourcesProvider.class, mProviders,
+                    resourcesProvider);
+            notifyProvidersChangedLocked();
+        }
+    }
+
+    /**
+     * Removes a provider from the provider list. If the provider is not present in the provider
+     * list, the list will not be modified.
+     *
+     * @param resourcesProvider the provider to remove
+     */
+    public void removeProvider(@NonNull ResourcesProvider resourcesProvider) {
+        synchronized (mLock) {
+            mProviders = ArrayUtils.removeElement(ResourcesProvider.class, mProviders,
+                    resourcesProvider);
+            notifyProvidersChangedLocked();
+        }
+    }
+
+    /**
+     * Sets the list of providers.
+     *
+     * @param resourcesProviders the new providers
+     */
+    public void setProviders(@NonNull List<ResourcesProvider> resourcesProviders) {
+        synchronized (mLock) {
+            mProviders = resourcesProviders.toArray(new ResourcesProvider[0]);
+            notifyProvidersChangedLocked();
+        }
+    }
+
+    /** Removes all {@link ResourcesProvider ResourcesProvider(s)}. */
+    public void clearProviders() {
+        synchronized (mLock) {
+            mProviders = null;
+            notifyProvidersChangedLocked();
+        }
+    }
+
+    /**
+     * Retrieves the list of {@link ApkAssets} used by the providers.
+     *
+     * @hide
+     */
+    @NonNull
+    public List<ApkAssets> getApkAssets() {
+        synchronized (mLock) {
+            if (mApkAssets == null) {
+                return Collections.emptyList();
+            }
+            return Arrays.asList(mApkAssets);
+        }
+    }
+
+    /**
+     * Registers a callback to be invoked when {@link ResourcesProvider ResourcesProvider(s)}
+     * change.
+     * @param instance the instance tied to the callback
+     * @param callbacks the callback to invoke
+     *
+     * @hide
+     */
+    public void registerOnProvidersChangedCallback(@NonNull Object instance,
+            @NonNull UpdateCallbacks callbacks) {
+        synchronized (mLock) {
+            mChangeCallbacks.put(new WeakReference<>(instance), callbacks);
+        }
+    }
+
+    /**
+     * Removes a previously registered callback.
+     * @param instance the instance tied to the callback
+     *
+     * @hide
+     */
+    public void unregisterOnProvidersChangedCallback(@NonNull Object instance) {
+        synchronized (mLock) {
+            for (int i = 0, n = mChangeCallbacks.size(); i < n; i++) {
+                final WeakReference<Object> key = mChangeCallbacks.keyAt(i);
+                if (instance == key.get()) {
+                    mChangeCallbacks.removeAt(i);
+                    return;
+                }
+            }
+        }
+    }
+
+    /** Returns whether the arrays contain the same provider instances in the same order. */
+    private static boolean arrayEquals(ResourcesProvider[] a1, ResourcesProvider[] a2) {
+        if (a1 == a2) {
+            return true;
+        }
+
+        if (a1 == null || a2 == null) {
+            return false;
+        }
+
+        if (a1.length != a2.length) {
+            return false;
+        }
+
+        // Check that the arrays contain the exact same instances in the same order. Providers do
+        // not have any form of equivalence checking of whether the contents of two providers have
+        // equivalent apk assets.
+        for (int i = 0, n = a1.length; i < n; i++) {
+            if (a1[i] != a2[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    /**
+     * Invokes registered callbacks when the list of {@link ResourcesProvider} instances this loader
+     * uses changes.
+     */
+    private void notifyProvidersChangedLocked() {
+        final ArraySet<UpdateCallbacks> uniqueCallbacks = new ArraySet<>();
+        if (arrayEquals(mPreviousProviders, mProviders)) {
+            return;
+        }
+
+        if (mProviders == null || mProviders.length == 0) {
+            mApkAssets = null;
+        } else {
+            mApkAssets = new ApkAssets[mProviders.length];
+            for (int i = 0, n = mProviders.length; i < n; i++) {
+                mProviders[i].incrementRefCount();
+                mApkAssets[i] = mProviders[i].getApkAssets();
+            }
+        }
+
+        // Decrement the ref count after incrementing the new provider ref count so providers
+        // present before and after this method do not drop to zero references.
+        if (mPreviousProviders != null) {
+            for (ResourcesProvider provider : mPreviousProviders) {
+                provider.decrementRefCount();
+            }
+        }
+
+        mPreviousProviders = mProviders;
+
+        for (int i = mChangeCallbacks.size() - 1; i >= 0; i--) {
+            final WeakReference<Object> key = mChangeCallbacks.keyAt(i);
+            if (key.get() == null) {
+                mChangeCallbacks.removeAt(i);
+            } else {
+                uniqueCallbacks.add(mChangeCallbacks.valueAt(i));
+            }
+        }
+
+        for (int i = 0, n = uniqueCallbacks.size(); i < n; i++) {
+            uniqueCallbacks.valueAt(i).onLoaderUpdated(this);
+        }
+    }
+}
diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java
index 050aeb7..419ec78 100644
--- a/core/java/android/content/res/loader/ResourcesProvider.java
+++ b/core/java/android/content/res/loader/ResourcesProvider.java
@@ -17,84 +17,144 @@
 package android.content.res.loader;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 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 android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 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.
+ * Provides methods to load resources data from APKs ({@code .apk}) and resources tables
+ * {@code .arsc} for use with {@link ResourcesLoader ResourcesLoader(s)}.
  */
-public final class ResourcesProvider implements AutoCloseable, Closeable {
+public class ResourcesProvider implements AutoCloseable, Closeable {
+    private static final String TAG = "ResourcesProvider";
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private boolean mOpen = true;
+
+    @GuardedBy("mLock")
+    private int mOpenCount = 0;
+
+    @GuardedBy("mLock")
+    private final ApkAssets mApkAssets;
+
+    private final AssetsProvider mAssetsProvider;
 
     /**
-     * Contains no data, assuming that any resource loading behavior will be handled in the
-     * corresponding {@link ResourceLoader}.
+     * Creates an empty ResourcesProvider with no resource data. This is useful for loading assets
+     * that are not associated with resource identifiers.
+     *
+     * @param assetsProvider the assets provider that overrides the loading of file-based resources
      */
     @NonNull
-    public static ResourcesProvider empty() {
-        return new ResourcesProvider(ApkAssets.loadEmptyForLoader());
+    public static ResourcesProvider empty(@NonNull AssetsProvider assetsProvider) {
+        return new ResourcesProvider(ApkAssets.loadEmptyForLoader(), assetsProvider);
     }
 
     /**
-     * Read from an .apk file descriptor.
+     * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor.
      *
-     * The file descriptor is duplicated and the one passed in may be closed by the application
-     * at any time.
+     * The file descriptor is duplicated and the original may be closed by the application at any
+     * time without affecting the ResourcesProvider.
+     *
+     * @param fileDescriptor the file descriptor of the APK to load
      */
     @NonNull
     public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor)
             throws IOException {
-        return new ResourcesProvider(
-                ApkAssets.loadApkForLoader(fileDescriptor.getFileDescriptor()));
+        return loadFromApk(fileDescriptor, null);
     }
 
     /**
-     * Read from an .apk file representation in memory.
+     * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor.
+     *
+     * The file descriptor is duplicated and the original may be closed by the application at any
+     * time without affecting the ResourcesProvider.
+     *
+     * @param fileDescriptor the file descriptor of the APK to load
+     * @param assetsProvider the assets provider that overrides the loading of file-based resources
+     */
+    @NonNull
+    public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor,
+            @Nullable AssetsProvider assetsProvider)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadApkForLoader(fileDescriptor.getFileDescriptor()), assetsProvider);
+    }
+
+    /**
+     * Creates a ResourcesProvider from an {@code .apk} file representation in memory.
+     *
+     * @param sharedMemory the shared memory containing the data of the APK to load
      */
     @NonNull
     public static ResourcesProvider loadFromApk(@NonNull SharedMemory sharedMemory)
             throws IOException {
-        return new ResourcesProvider(
-                ApkAssets.loadApkForLoader(sharedMemory.getFileDescriptor()));
+        return loadFromApk(sharedMemory, null);
     }
 
     /**
-     * Read from an .arsc file descriptor.
+     * Creates a ResourcesProvider from an {@code .apk} file representation in memory.
      *
-     * The file descriptor is duplicated and the one passed in may be closed by the application
-     * at any time.
+     * @param sharedMemory the shared memory containing the data of the APK to load
+     * @param assetsProvider the assets provider that implements the loading of file-based resources
      */
     @NonNull
-    public static ResourcesProvider loadFromArsc(@NonNull ParcelFileDescriptor fileDescriptor)
+    public static ResourcesProvider loadFromApk(@NonNull SharedMemory sharedMemory,
+            @Nullable AssetsProvider assetsProvider)
             throws IOException {
         return new ResourcesProvider(
-                ApkAssets.loadArscForLoader(fileDescriptor.getFileDescriptor()));
+                ApkAssets.loadApkForLoader(sharedMemory.getFileDescriptor()), assetsProvider);
     }
 
     /**
-     * Read from an .arsc file representation in memory.
+     * Creates a ResourcesProvider from a resources table ({@code .arsc}) file descriptor.
+     *
+     * The file descriptor is duplicated and the original may be closed by the application at any
+     * time without affecting the ResourcesProvider.
+     *
+     * @param fileDescriptor the file descriptor of the resources table to load
+     * @param assetsProvider the assets provider that implements the loading of file-based resources
      */
     @NonNull
-    public static ResourcesProvider loadFromArsc(@NonNull SharedMemory sharedMemory)
+    public static ResourcesProvider loadFromTable(@NonNull ParcelFileDescriptor fileDescriptor,
+            @Nullable AssetsProvider assetsProvider)
             throws IOException {
         return new ResourcesProvider(
-                ApkAssets.loadArscForLoader(sharedMemory.getFileDescriptor()));
+                ApkAssets.loadArscForLoader(fileDescriptor.getFileDescriptor()), assetsProvider);
+    }
+
+    /**
+     * Creates a ResourcesProvider from a resources table ({@code .arsc}) file representation in
+     * memory.
+     *
+     * @param sharedMemory the shared memory containing the data of the resources table to load
+     * @param assetsProvider the assets provider that overrides the loading of file-based resources
+     */
+    @NonNull
+    public static ResourcesProvider loadFromTable(@NonNull SharedMemory sharedMemory,
+            @Nullable AssetsProvider assetsProvider)
+            throws IOException {
+        return new ResourcesProvider(
+                ApkAssets.loadArscForLoader(sharedMemory.getFileDescriptor()), assetsProvider);
     }
 
     /**
      * Read from a split installed alongside the application, which may not have been
      * loaded initially because the application requested isolated split loading.
+     *
+     * @param context a context of the package that contains the split
+     * @param splitName the name of the split to load
      */
     @NonNull
     public static ResourcesProvider loadFromSplit(@NonNull Context context,
@@ -106,15 +166,18 @@
         }
 
         String splitPath = appInfo.getSplitCodePaths()[splitIndex];
-        return new ResourcesProvider(ApkAssets.loadApkForLoader(splitPath));
+        return new ResourcesProvider(ApkAssets.loadApkForLoader(splitPath), null);
     }
 
-
-    @NonNull
-    private final ApkAssets mApkAssets;
-
-    private ResourcesProvider(@NonNull ApkAssets apkAssets) {
+    private ResourcesProvider(@NonNull ApkAssets apkAssets,
+            @Nullable AssetsProvider assetsProvider) {
         this.mApkAssets = apkAssets;
+        this.mAssetsProvider = assetsProvider;
+    }
+
+    @Nullable
+    public AssetsProvider getAssetsProvider() {
+        return mAssetsProvider;
     }
 
     /** @hide */
@@ -123,8 +186,41 @@
         return mApkAssets;
     }
 
+    final void incrementRefCount() {
+        synchronized (mLock) {
+            if (!mOpen) {
+                throw new IllegalStateException("Operation failed: resources provider is closed");
+            }
+            mOpenCount++;
+        }
+    }
+
+    final void decrementRefCount() {
+        synchronized (mLock) {
+            mOpenCount--;
+        }
+    }
+
+    /**
+     * Frees internal data structures. Closed providers can no longer be added to
+     * {@link ResourcesLoader ResourcesLoader(s)}.
+     *
+     * @throws IllegalStateException if provider is currently used by a ResourcesLoader
+     */
     @Override
     public void close() {
+        synchronized (mLock) {
+            if (!mOpen) {
+                return;
+            }
+
+            if (mOpenCount != 0) {
+                throw new IllegalStateException("Failed to close provider used by " + mOpenCount
+                        + " ResourcesLoader instances");
+            }
+            mOpen = false;
+        }
+
         try {
             mApkAssets.close();
         } catch (Throwable ignored) {
@@ -133,7 +229,16 @@
 
     @Override
     protected void finalize() throws Throwable {
-        close();
-        super.finalize();
+        synchronized (mLock) {
+            if (mOpenCount != 0) {
+                Log.w(TAG, "ResourcesProvider " + this + " finalized with non-zero refs: "
+                        + mOpenCount);
+            }
+
+            if (mOpen) {
+                mOpen = false;
+                mApkAssets.close();
+            }
+        }
     }
 }
diff --git a/core/tests/ResourceLoaderTests/Android.bp b/core/tests/ResourceLoaderTests/Android.bp
index 53db832..fec4628 100644
--- a/core/tests/ResourceLoaderTests/Android.bp
+++ b/core/tests/ResourceLoaderTests/Android.bp
@@ -32,15 +32,16 @@
         "truth-prebuilt",
     ],
     resource_zips: [ ":FrameworksResourceLoaderTestsAssets" ],
+    platform_apis: true,
     test_suites: ["device-tests"],
-    sdk_version: "test_current",
     aaptflags: [
         "--no-compress",
     ],
     data: [
-        ":FrameworksResourceLoaderTestsOverlay",
         ":FrameworksResourceLoaderTestsSplitOne",
         ":FrameworksResourceLoaderTestsSplitTwo",
+        ":FrameworksResourceLoaderTestsSplitThree",
+        ":FrameworksResourceLoaderTestsSplitFour",
     ],
     java_resources: [ "NonAsset.txt" ]
 }
diff --git a/core/tests/ResourceLoaderTests/AndroidTest.xml b/core/tests/ResourceLoaderTests/AndroidTest.xml
index 702151d..d732132 100644
--- a/core/tests/ResourceLoaderTests/AndroidTest.xml
+++ b/core/tests/ResourceLoaderTests/AndroidTest.xml
@@ -22,13 +22,7 @@
         <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" />
+            value="FrameworksResourceLoaderTests.apk,FrameworksResourceLoaderTestsSplitOne.apk,FrameworksResourceLoaderTestsSplitTwo.apk,FrameworksResourceLoaderTestsSplitThree.apk,FrameworksResourceLoaderTestsSplitFour.apk" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest">
diff --git a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png
index efd71ee..8102d15 100644
--- a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png
+++ b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/res/layout/layout.xml b/core/tests/ResourceLoaderTests/res/layout/layout.xml
index d59059b..05499ed 100644
--- a/core/tests/ResourceLoaderTests/res/layout/layout.xml
+++ b/core/tests/ResourceLoaderTests/res/layout/layout.xml
@@ -15,7 +15,7 @@
   ~ limitations under the License.
   -->
 
-<FrameLayout
+<MysteryLayout
     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/compileAndLink.sh b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh
index 885f681..8e05aef 100755
--- a/core/tests/ResourceLoaderTests/resources/compileAndLink.sh
+++ b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh
@@ -68,9 +68,13 @@
 
 compileAndLink stringOne BOTH AndroidManifestFramework.xml res/values/string_one.xml
 compileAndLink stringTwo BOTH AndroidManifestFramework.xml res/values/string_two.xml
+compileAndLink stringThree BOTH AndroidManifestFramework.xml res/values/string_three.xml
+compileAndLink stringFour BOTH AndroidManifestFramework.xml res/values/string_four.xml
 
 compileAndLink dimenOne BOTH AndroidManifestFramework.xml res/values/dimen_one.xml
 compileAndLink dimenTwo BOTH AndroidManifestFramework.xml res/values/dimen_two.xml
+compileAndLink dimenThree BOTH AndroidManifestFramework.xml res/values/dimen_three.xml
+compileAndLink dimenFour BOTH AndroidManifestFramework.xml res/values/dimen_four.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
@@ -86,6 +90,14 @@
 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
 
+cp -f "$inDir"/res/layout/layout_three.xml "$genDir"/temp/res/layout/layout.xml
+compileAndLink layoutThree ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml
+cp -f "$genDir"/out/layoutThree/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutThree.xml
+
+cp -f "$inDir"/res/layout/layout_four.xml "$genDir"/temp/res/layout/layout.xml
+compileAndLink layoutFour ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml
+cp -f "$genDir"/out/layoutFour/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutFour.xml
+
 drawableNoDpi="/res/drawable-nodpi"
 inDirDrawableNoDpi="$inDir$drawableNoDpi"
 
@@ -97,6 +109,18 @@
 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"/nonAssetDrawableThree.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml
+compileAndLink nonAssetDrawableThree ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml
+cp -f "$genDir"/out/nonAssetDrawableThree/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableThree.xml
+
+cp -f "$inDirDrawableNoDpi"/nonAssetDrawableFour.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml
+compileAndLink nonAssetDrawableFour ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml
+cp -f "$genDir"/out/nonAssetDrawableFour/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableFour.xml
+
+cp -f "$inDirDrawableNoDpi"/nonAssetBitmapRed.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png
+compileAndLink nonAssetBitmapRed BOTH AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml
+cp -f "$genDir"/out/nonAssetBitmapRed/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapRed.png
+
 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
@@ -105,4 +129,8 @@
 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
 
+cp -f "$inDirDrawableNoDpi"/nonAssetBitmapWhite.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png
+compileAndLink nonAssetBitmapWhite ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml
+cp -f "$genDir"/out/nonAssetBitmapWhite/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapWhite.png
+
 $soong_zip -o "$genDir"/out.zip -C "$genDir"/output/ -D "$genDir"/output/
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapRed.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapRed.png
new file mode 100644
index 0000000..4eb8ca3
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapRed.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapWhite.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapWhite.png
new file mode 100644
index 0000000..e9a4cfc
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapWhite.png
Binary files differ
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableFour.xml
similarity index 78%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableFour.xml
index 348bb35..0623245 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableFour.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -15,8 +15,7 @@
   ~ limitations under the License.
   -->
 
-<resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
-</resources>
+<color
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#000004"
+    />
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml
index f1a93d2..57a8cf1 100644
--- a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml
@@ -17,5 +17,5 @@
 
 <color
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="#A3C3E3"
+    android:color="#000001"
     />
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableThree.xml
similarity index 78%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableThree.xml
index 348bb35..41095d4 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableThree.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -15,8 +15,7 @@
   ~ limitations under the License.
   -->
 
-<resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
-</resources>
+<color
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#000003"
+    />
diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml
index 7c455a5..333fe34 100644
--- a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml
@@ -17,5 +17,5 @@
 
 <color
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="#3A3C3E"
+    android:color="#000002"
     />
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_four.xml
similarity index 73%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/layout/layout_four.xml
index 348bb35..ab9e265 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_four.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -15,8 +15,9 @@
   ~ limitations under the License.
   -->
 
-<resources>
+<TableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
 
-    <string name="loader_path_change_test">Overlaid</string>
-
-</resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_three.xml
similarity index 73%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/layout/layout_three.xml
index 348bb35..d58d3db 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_three.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -15,8 +15,9 @@
   ~ limitations under the License.
   -->
 
-<resources>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    />
 
-    <string name="loader_path_change_test">Overlaid</string>
-
-</resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_four.xml
similarity index 79%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/values/dimen_four.xml
index 348bb35..5b30eba 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_four.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="dimen" name="app_icon_size" id="0x01050000" />
+    <dimen name="app_icon_size">400dp</dimen>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml
index 69ecf23..b17ec1c 100644
--- a/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml
@@ -17,5 +17,5 @@
 
 <resources>
     <public type="dimen" name="app_icon_size" id="0x01050000" />
-    <dimen name="app_icon_size">564716dp</dimen>
+    <dimen name="app_icon_size">100dp</dimen>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_three.xml
similarity index 79%
rename from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
rename to core/tests/ResourceLoaderTests/resources/res/values/dimen_three.xml
index 348bb35..07a35ce 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_three.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="dimen" name="app_icon_size" id="0x01050000" />
+    <dimen name="app_icon_size">300dp</dimen>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml
index 4d55def..570b40a 100644
--- a/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml
@@ -17,5 +17,5 @@
 
 <resources>
     <public type="dimen" name="app_icon_size" id="0x01050000" />
-    <dimen name="app_icon_size">565717dp</dimen>
+    <dimen name="app_icon_size">200dp</dimen>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_four.xml
similarity index 77%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/values/string_four.xml
index 348bb35..8789bcd 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/string_four.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="string" name="cancel" id="0x01040000" />
+    <string name="cancel">SomeRidiculouslyUnlikelyStringFour</string>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_three.xml
similarity index 77%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/resources/res/values/string_three.xml
index 348bb35..82cd6ec 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/resources/res/values/string_three.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="string" name="cancel" id="0x01040000" />
+    <string name="cancel">SomeRidiculouslyUnlikelyStringThree</string>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/overlay/Android.bp b/core/tests/ResourceLoaderTests/splits/SplitFour/Android.bp
similarity index 74%
rename from core/tests/ResourceLoaderTests/overlay/Android.bp
rename to core/tests/ResourceLoaderTests/splits/SplitFour/Android.bp
index 63e7e61..eb4d8e1 100644
--- a/core/tests/ResourceLoaderTests/overlay/Android.bp
+++ b/core/tests/ResourceLoaderTests/splits/SplitFour/Android.bp
@@ -1,4 +1,5 @@
-// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
@@ -11,10 +12,8 @@
 // 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"],
+android_test_helper_app {
+    name: "FrameworksResourceLoaderTestsSplitFour"
 }
diff --git a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/SplitFour/AndroidManifest.xml
similarity index 79%
rename from core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
rename to core/tests/ResourceLoaderTests/splits/SplitFour/AndroidManifest.xml
index 942f7da..24a0a2a 100644
--- a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
+++ b/core/tests/ResourceLoaderTests/splits/SplitFour/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -17,11 +17,11 @@
 
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.res.loader.test.overlay"
+    package="android.content.res.loader.test"
+    split="split_four"
     >
 
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
     <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/splits/SplitFour/res/values/string_split.xml
similarity index 77%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/splits/SplitFour/res/values/string_split.xml
index 348bb35..4759db9 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/splits/SplitFour/res/values/string_split.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="string" name="split_overlaid" id="0x7f040001" />
+    <string name="split_overlaid">Split FOUR Overlaid</string>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/SplitOne/Android.bp b/core/tests/ResourceLoaderTests/splits/SplitOne/Android.bp
similarity index 100%
rename from core/tests/ResourceLoaderTests/SplitOne/Android.bp
rename to core/tests/ResourceLoaderTests/splits/SplitOne/Android.bp
diff --git a/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/SplitOne/AndroidManifest.xml
similarity index 100%
rename from core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml
rename to core/tests/ResourceLoaderTests/splits/SplitOne/AndroidManifest.xml
diff --git a/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml b/core/tests/ResourceLoaderTests/splits/SplitOne/res/values/string_split.xml
similarity index 100%
rename from core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml
rename to core/tests/ResourceLoaderTests/splits/SplitOne/res/values/string_split.xml
diff --git a/core/tests/ResourceLoaderTests/overlay/Android.bp b/core/tests/ResourceLoaderTests/splits/SplitThree/Android.bp
similarity index 74%
copy from core/tests/ResourceLoaderTests/overlay/Android.bp
copy to core/tests/ResourceLoaderTests/splits/SplitThree/Android.bp
index 63e7e61..bf98a74 100644
--- a/core/tests/ResourceLoaderTests/overlay/Android.bp
+++ b/core/tests/ResourceLoaderTests/splits/SplitThree/Android.bp
@@ -1,4 +1,5 @@
-// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
@@ -11,10 +12,8 @@
 // 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"],
+android_test_helper_app {
+    name: "FrameworksResourceLoaderTestsSplitThree"
 }
diff --git a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/SplitThree/AndroidManifest.xml
similarity index 79%
copy from core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
copy to core/tests/ResourceLoaderTests/splits/SplitThree/AndroidManifest.xml
index 942f7da..ae1579b 100644
--- a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml
+++ b/core/tests/ResourceLoaderTests/splits/SplitThree/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -17,11 +17,11 @@
 
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.res.loader.test.overlay"
+    package="android.content.res.loader.test"
+    split="split_three"
     >
 
+    <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" />
     <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/splits/SplitThree/res/values/string_spli.xml
similarity index 77%
copy from core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
copy to core/tests/ResourceLoaderTests/splits/SplitThree/res/values/string_spli.xml
index 348bb35..97682aa 100644
--- a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml
+++ b/core/tests/ResourceLoaderTests/splits/SplitThree/res/values/string_spli.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright (C) 2020 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.
@@ -16,7 +16,6 @@
   -->
 
 <resources>
-
-    <string name="loader_path_change_test">Overlaid</string>
-
+    <public type="string" name="split_overlaid" id="0x7f040001" />
+    <string name="split_overlaid">Split THREE Overlaid</string>
 </resources>
diff --git a/core/tests/ResourceLoaderTests/splits/Android.bp b/core/tests/ResourceLoaderTests/splits/SplitTwo/Android.bp
similarity index 100%
rename from core/tests/ResourceLoaderTests/splits/Android.bp
rename to core/tests/ResourceLoaderTests/splits/SplitTwo/Android.bp
diff --git a/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/SplitTwo/AndroidManifest.xml
similarity index 100%
rename from core/tests/ResourceLoaderTests/splits/AndroidManifest.xml
rename to core/tests/ResourceLoaderTests/splits/SplitTwo/AndroidManifest.xml
diff --git a/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml b/core/tests/ResourceLoaderTests/splits/SplitTwo/res/values/string_split.xml
similarity index 100%
rename from core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml
rename to core/tests/ResourceLoaderTests/splits/SplitTwo/res/values/string_split.xml
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryAssetsProviderTest.kt
similarity index 68%
rename from core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt
rename to core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryAssetsProviderTest.kt
index b1bdc96..9e94bdc 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryAssetsProviderTest.kt
@@ -16,8 +16,9 @@
 
 package android.content.res.loader.test
 
-import android.content.res.loader.DirectoryResourceLoader
-import android.content.res.loader.ResourceLoader
+import android.content.res.loader.AssetsProvider
+import android.content.res.loader.DirectoryAssetsProvider
+import android.content.res.loader.ResourcesLoader
 import android.graphics.Color
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.ColorDrawable
@@ -29,18 +30,21 @@
 import org.junit.rules.TestName
 import java.io.File
 
-class DirectoryResourceLoaderTest : ResourceLoaderTestBase() {
+class DirectoryAssetsProviderTest : ResourceLoaderTestBase() {
 
     @get:Rule
     val testName = TestName()
 
     private lateinit var testDir: File
-    private lateinit var loader: ResourceLoader
+    private lateinit var assetsProvider: AssetsProvider
+    private lateinit var loader: ResourcesLoader
 
     @Before
     fun setUpTestDir() {
-        testDir = context.filesDir.resolve("DirectoryResourceLoaderTest_${testName.methodName}")
-        loader = DirectoryResourceLoader(testDir)
+        testDir = context.filesDir.resolve("DirectoryAssetsProvider_${testName.methodName}")
+        assetsProvider = DirectoryAssetsProvider(testDir)
+        loader = ResourcesLoader()
+        resources.addLoader(loader)
     }
 
     @After
@@ -51,29 +55,29 @@
     @Test
     fun loadDrawableXml() {
         "nonAssetDrawableOne" writeTo "res/drawable-nodpi-v4/non_asset_drawable.xml"
-        val provider = openArsc("nonAssetDrawableOne")
+        val provider = openArsc("nonAssetDrawableOne", assetsProvider)
 
         fun getValue() = (resources.getDrawable(R.drawable.non_asset_drawable) as ColorDrawable)
                 .color
 
         assertThat(getValue()).isEqualTo(Color.parseColor("#B2D2F2"))
 
-        addLoader(loader to provider)
+        loader.addProvider(provider)
 
-        assertThat(getValue()).isEqualTo(Color.parseColor("#A3C3E3"))
+        assertThat(getValue()).isEqualTo(Color.parseColor("#000001"))
     }
 
     @Test
     fun loadDrawableBitmap() {
         "nonAssetBitmapGreen" writeTo "res/drawable-nodpi-v4/non_asset_bitmap.png"
-        val provider = openArsc("nonAssetBitmapGreen")
+        val provider = openArsc("nonAssetBitmapGreen", assetsProvider)
 
         fun getValue() = (resources.getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable)
                 .bitmap.getColor(0, 0).toArgb()
 
-        assertThat(getValue()).isEqualTo(Color.RED)
+        assertThat(getValue()).isEqualTo(Color.MAGENTA)
 
-        addLoader(loader to provider)
+        loader.addProvider(provider)
 
         assertThat(getValue()).isEqualTo(Color.GREEN)
     }
@@ -81,13 +85,13 @@
     @Test
     fun loadXml() {
         "layoutOne" writeTo "res/layout/layout.xml"
-        val provider = openArsc("layoutOne")
+        val provider = openArsc("layoutOne", assetsProvider)
 
         fun getValue() = resources.getLayout(R.layout.layout).advanceToRoot().name
 
-        assertThat(getValue()).isEqualTo("FrameLayout")
+        assertThat(getValue()).isEqualTo("MysteryLayout")
 
-        addLoader(loader to provider)
+        loader.addProvider(provider)
 
         assertThat(getValue()).isEqualTo("RelativeLayout")
     }
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
deleted file mode 100644
index a6a8378..0000000
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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/ResourceLoaderAssetsTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetsTest.kt
new file mode 100644
index 0000000..e3ba93d
--- /dev/null
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetsTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.AssetsProvider
+import android.content.res.loader.DirectoryAssetsProvider
+import android.content.res.loader.ResourcesLoader
+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 ResourceLoaderAssetsTest : 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: AssetsProvider.(String) -> Any? = {
+                loadAsset(eq(it), anyInt())
+            }
+
+            val fromFileDescriptor: AssetsProvider.(String) -> Any? = {
+                loadAssetParcelFd(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: AssetsProvider.(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 AssetsProvider.loadAsset() = loadAssetFunction(testPath)
+
+    private fun AssetManager.openAsset() = openAssetFunction()
+
+    private lateinit var testDir: File
+
+    @Before
+    fun setUpTestDir() {
+        testDir = context.filesDir.resolve("DirectoryAssetsProvider_${testName.methodName}")
+        testDir.resolve(testPath).apply { parentFile!!.mkdirs() }.writeText(TEST_TEXT)
+    }
+
+    @Test
+    fun multipleProvidersSearchesBackwards() {
+        // DirectoryResourceLoader relies on a private field and can't be spied directly, so wrap it
+        val assetsProvider = DirectoryAssetsProvider(testDir)
+        val assetProviderWrapper = mock(AssetsProvider::class.java).apply {
+            doAnswer { assetsProvider.loadAsset(it.arguments[0] as String, it.arguments[1] as Int) }
+                    .`when`(this).loadAsset(anyString(), anyInt())
+            doAnswer { assetsProvider.loadAssetParcelFd(it.arguments[0] as String) }
+                    .`when`(this).loadAssetParcelFd(anyString())
+        }
+
+        val one = ResourcesProvider.empty(assetProviderWrapper)
+        val two = mockProvider {
+            doReturn(null).`when`(it).loadAsset()
+        }
+
+        val loader = ResourcesLoader()
+        loader.providers = listOf(one, two)
+        resources.addLoader(loader)
+
+        assertOpenedAsset()
+        inOrder(two.assetsProvider, one.assetsProvider).apply {
+            verify(two.assetsProvider)?.loadAsset()
+            verify(one.assetsProvider)?.loadAsset()
+        }
+    }
+
+    @Test
+    fun multipleLoadersSearchesBackwards() {
+        // DirectoryResourceLoader relies on a private field and can't be spied directly, so wrap it
+        val assetsProvider = DirectoryAssetsProvider(testDir)
+        val assetProviderWrapper = mock(AssetsProvider::class.java).apply {
+            doAnswer { assetsProvider.loadAsset(it.arguments[0] as String, it.arguments[1] as Int) }
+                    .`when`(this).loadAsset(anyString(), anyInt())
+            doAnswer { assetsProvider.loadAssetParcelFd(it.arguments[0] as String) }
+                    .`when`(this).loadAssetParcelFd(anyString())
+        }
+
+        val one = ResourcesProvider.empty(assetProviderWrapper)
+        val two = mockProvider {
+            doReturn(null).`when`(it).loadAsset()
+        }
+
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(one)
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(two)
+
+        resources.loaders = listOf(loader1, loader2)
+
+        assertOpenedAsset()
+        inOrder(two.assetsProvider, one.assetsProvider).apply {
+            verify(two.assetsProvider)?.loadAsset()
+            verify(one.assetsProvider)?.loadAsset()
+        }
+    }
+
+    @Test(expected = FileNotFoundException::class)
+    fun failToFindThrowsFileNotFound() {
+        val assetsProvider1 = mock(AssetsProvider::class.java).apply {
+            doReturn(null).`when`(this).loadAsset()
+        }
+        val assetsProvider2 = mock(AssetsProvider::class.java).apply {
+            doReturn(null).`when`(this).loadAsset()
+        }
+
+        val loader = ResourcesLoader()
+        val one = ResourcesProvider.empty(assetsProvider1)
+        val two = ResourcesProvider.empty(assetsProvider2)
+        resources.addLoader(loader)
+        loader.providers = listOf(one, two)
+
+        assertOpenedAsset()
+    }
+
+    @Test
+    fun throwingIOExceptionIsSkipped() {
+        val assetsProvider1 = DirectoryAssetsProvider(testDir)
+        val assetsProvider2 = mock(AssetsProvider::class.java).apply {
+            doAnswer { throw IOException() }.`when`(this).loadAsset()
+        }
+
+        val loader = ResourcesLoader()
+        val one = ResourcesProvider.empty(assetsProvider1)
+        val two = ResourcesProvider.empty(assetsProvider2)
+        resources.addLoader(loader)
+        loader.providers = listOf(one, two)
+
+        assertOpenedAsset()
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun throwingNonIOExceptionCausesFailure() {
+        val assetsProvider1 = DirectoryAssetsProvider(testDir)
+        val assetsProvider2 = mock(AssetsProvider::class.java).apply {
+            doAnswer { throw IllegalStateException() }.`when`(this).loadAsset()
+        }
+
+        val loader = ResourcesLoader()
+        val one = ResourcesProvider.empty(assetsProvider1)
+        val two = ResourcesProvider.empty(assetsProvider2)
+        resources.addLoader(loader)
+        loader.providers = listOf(one, two)
+
+        assertOpenedAsset()
+    }
+
+    private fun mockProvider(block: (AssetsProvider) -> Unit = {}): ResourcesProvider {
+        return ResourcesProvider.empty(mock(AssetsProvider::class.java).apply {
+            block.invoke(this)
+        })
+    }
+
+    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
deleted file mode 100644
index 0c3d34e..0000000
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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.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
-
-@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) {
-        fun Resources.resource() = this.getString(android.R.string.cancel)
-        fun Resources.asset() = this.assets.open("Asset.txt").reader().readText()
-
-        val originalResource = resources.resource()
-        val originalAsset = resources.asset()
-
-        val loaderResource = "stringOne".openLoader()
-        val loaderAsset = "assetOne".openLoader(dataType = DataType.ASSET)
-        addLoader(loaderResource)
-        addLoader(loaderAsset)
-
-        val oldLoaders = resources.loaders
-        val oldResource = resources.resource()
-        val oldAsset = resources.asset()
-
-        assertThat(oldResource).isNotEqualTo(originalResource)
-        assertThat(oldAsset).isNotEqualTo(originalAsset)
-
-        val newResources = block()
-
-        val newLoaders = newResources.loaders
-        val newResource = newResources.resource()
-        val newAsset = newResources.asset()
-
-        assertThat(newResource).isEqualTo(oldResource)
-        assertThat(newAsset).isEqualTo(oldAsset)
-        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
deleted file mode 100644
index 09fd27e..0000000
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 1ec2094..0000000
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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
index 5af453d..4c62955 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt
@@ -18,25 +18,16 @@
 
 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.AssetsProvider
 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
 
@@ -60,7 +51,8 @@
 
     @After
     fun removeAllLoaders() {
-        resources.setLoaders(null)
+        resources.clearLoaders()
+        context.applicationContext.resources.clearLoaders()
         openedObjects.forEach {
             try {
                 it.close()
@@ -69,149 +61,73 @@
         }
     }
 
-    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(
+    protected fun String.openProvider(
         dataType: DataType = this@ResourceLoaderTestBase.dataType
-    ): Pair<ResourceLoader, ResourcesProvider> = when (dataType) {
+    ): ResourcesProvider = when (dataType) {
         DataType.APK -> {
-            mock(ResourceLoader::class.java) to context.copiedRawFile("${this}Apk").use {
-                ResourcesProvider.loadFromApk(it)
+            context.copiedRawFile("${this}Apk").use {
+                ResourcesProvider.loadFromApk(it, mock(AssetsProvider::class.java))
             }.also { openedObjects += it }
         }
         DataType.ARSC -> {
-            mock(ResourceLoader::class.java) to openArsc(this)
+            openArsc(this, mock(AssetsProvider::class.java))
         }
         DataType.SPLIT -> {
-            mock(ResourceLoader::class.java) to ResourcesProvider.loadFromSplit(context, this)
+            ResourcesProvider.loadFromSplit(context, this)
         }
-        DataType.ASSET -> mockLoader {
-            doAnswer { byteInputStream() }.`when`(it)
+        DataType.ASSET -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
+            doAnswer { byteInputStream() }.`when`(assetsProvider)
                     .loadAsset(eq("assets/Asset.txt"), anyInt())
+            ResourcesProvider.empty(assetsProvider)
         }
-        DataType.ASSET_FD -> mockLoader {
+        DataType.ASSET_FD -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
             doAnswer {
                 val file = context.filesDir.resolve("Asset.txt")
                 file.writeText(this)
                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
-            }.`when`(it).loadAssetFd("assets/Asset.txt")
+            }.`when`(assetsProvider).loadAssetParcelFd("assets/Asset.txt")
+            ResourcesProvider.empty(assetsProvider)
         }
-        DataType.NON_ASSET -> mockLoader {
+        DataType.NON_ASSET -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
             doAnswer {
                 val file = context.filesDir.resolve("NonAsset.txt")
                 file.writeText(this)
                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
-            }.`when`(it).loadAssetFd("NonAsset.txt")
+            }.`when`(assetsProvider).loadAssetParcelFd("NonAsset.txt")
+            ResourcesProvider.empty(assetsProvider)
         }
-        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_DRAWABLE -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
+            doAnswer { context.copiedRawFile(this) }.`when`(assetsProvider)
+                    .loadAssetParcelFd("res/drawable-nodpi-v4/non_asset_drawable.xml")
+            openArsc(this, assetsProvider)
         }
-        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)
+        DataType.NON_ASSET_BITMAP -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
+            doAnswer { resources.openRawResource(rawFile(this)) }
+                    .`when`(assetsProvider)
                     .loadAsset(eq("res/drawable-nodpi-v4/non_asset_bitmap.png"), anyInt())
+            openArsc(this, assetsProvider)
         }
-        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")
+        DataType.NON_ASSET_LAYOUT -> {
+            val assetsProvider = mock(AssetsProvider::class.java)
+            doAnswer { resources.openRawResource(rawFile(this)) }.`when`(assetsProvider)
+                    .loadAsset(eq("res/layout/layout.xml"), anyInt())
+            doAnswer { context.copiedRawFile(this) }.`when`(assetsProvider)
+                    .loadAssetParcelFd("res/layout/layout.xml")
+            openArsc(this, assetsProvider)
         }
     }
 
-    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 {
+    protected fun openArsc(rawName: String, assetsProvider: AssetsProvider): ResourcesProvider {
         return context.copiedRawFile("${rawName}Arsc")
-                .use { ResourcesProvider.loadFromArsc(it) }
+                .use { ResourcesProvider.loadFromTable(it, assetsProvider) }
                 .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,
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
index 017552a..0cc56d7 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt
@@ -16,15 +16,24 @@
 
 package android.content.res.loader.test
 
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.content.res.loader.ResourcesLoader
 import android.graphics.Color
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.ColorDrawable
-import com.google.common.truth.Truth.assertThat
+import android.os.IBinder
+import androidx.test.rule.ActivityTestRule
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import java.util.Collections
 
 /**
  * Tests generic ResourceLoader behavior. Intentionally abstract in its test methodology because
@@ -36,6 +45,9 @@
 @RunWith(Parameterized::class)
 class ResourceLoaderValuesTest : ResourceLoaderTestBase() {
 
+    @get:Rule
+    private val mTestActivityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+
     companion object {
         @Parameterized.Parameters(name = "{1} {0}")
         @JvmStatic
@@ -47,14 +59,18 @@
                     { getString(android.R.string.cancel) },
                     "stringOne", { "SomeRidiculouslyUnlikelyStringOne" },
                     "stringTwo", { "SomeRidiculouslyUnlikelyStringTwo" },
+                    "stringThree", { "SomeRidiculouslyUnlikelyStringThree" },
+                    "stringFour", { "SomeRidiculouslyUnlikelyStringFour" },
                     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) },
+                    { getDimensionPixelSize(android.R.dimen.app_icon_size) },
+                    "dimenOne", { 100.dpToPx(resources) },
+                    "dimenTwo", { 200.dpToPx(resources) },
+                    "dimenThree", { 300.dpToPx(resources) },
+                    "dimenFour", { 400.dpToPx(resources) },
                     listOf(DataType.APK, DataType.ARSC)
             )
 
@@ -63,6 +79,8 @@
                     { assets.open("Asset.txt").reader().readText() },
                     "assetOne", { "assetOne" },
                     "assetTwo", { "assetTwo" },
+                    "assetFour", { "assetFour" },
+                    "assetThree", { "assetThree" },
                     listOf(DataType.ASSET)
             )
 
@@ -71,6 +89,8 @@
                     { assets.openFd("Asset.txt").readText() },
                     "assetOne", { "assetOne" },
                     "assetTwo", { "assetTwo" },
+                    "assetFour", { "assetFour" },
+                    "assetThree", { "assetThree" },
                     listOf(DataType.ASSET_FD)
             )
 
@@ -79,14 +99,18 @@
                     { assets.openNonAssetFd("NonAsset.txt").readText() },
                     "NonAssetOne", { "NonAssetOne" },
                     "NonAssetTwo", { "NonAssetTwo" },
+                    "NonAssetThree", { "NonAssetThree" },
+                    "NonAssetFour", { "NonAssetFour" },
                     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") },
+                    "nonAssetDrawableOne", { Color.parseColor("#000001") },
+                    "nonAssetDrawableTwo", { Color.parseColor("#000002") },
+                    "nonAssetDrawableThree", { Color.parseColor("#000003") },
+                    "nonAssetDrawableFour", { Color.parseColor("#000004") },
                     listOf(DataType.NON_ASSET_DRAWABLE)
             )
 
@@ -96,8 +120,10 @@
                         (getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable)
                                 .bitmap.getColor(0, 0).toArgb()
                     },
+                    "nonAssetBitmapRed", { Color.RED },
                     "nonAssetBitmapGreen", { Color.GREEN },
                     "nonAssetBitmapBlue", { Color.BLUE },
+                    "nonAssetBitmapWhite", { Color.WHITE },
                     listOf(DataType.NON_ASSET_BITMAP)
             )
 
@@ -106,6 +132,8 @@
                     { getLayout(R.layout.layout).advanceToRoot().name },
                     "layoutOne", { "RelativeLayout" },
                     "layoutTwo", { "LinearLayout" },
+                    "layoutThree", { "FrameLayout" },
+                    "layoutFour", { "TableLayout" },
                     listOf(DataType.NON_ASSET_LAYOUT)
             )
 
@@ -114,6 +142,8 @@
                     { getString(R.string.split_overlaid) },
                     "split_one", { "Split ONE Overlaid" },
                     "split_two", { "Split TWO Overlaid" },
+                    "split_three", { "Split THREE Overlaid" },
+                    "split_four", { "Split FOUR Overlaid" },
                     listOf(DataType.SPLIT)
             )
 
@@ -134,19 +164,52 @@
 
     private val valueOne by lazy { parameter.valueOne(this) }
     private val valueTwo by lazy { parameter.valueTwo(this) }
+    private val valueThree by lazy { parameter.valueThree(this) }
+    private val valueFour by lazy { parameter.valueFour(this) }
 
-    private fun openOne() = parameter.loaderOne.openLoader()
-    private fun openTwo() = parameter.loaderTwo.openLoader()
+    private fun openOne() = parameter.providerOne.openProvider()
+    private fun openTwo() = parameter.providerTwo.openProvider()
+    private fun openThree() = parameter.providerThree.openProvider()
+    private fun openFour() = parameter.providerFour.openProvider()
 
     // Class method for syntax highlighting purposes
-    private fun getValue() = parameter.getValue(this)
+    private fun getValue(c: Context = context) = parameter.getValue(c.resources)
 
     @Test
-    fun verifyValueUniqueness() {
+    fun assertValueUniqueness() {
         // Ensure the parameters are valid in case of coding errors
-        assertNotEquals(valueOne, getValue())
-        assertNotEquals(valueTwo, getValue())
-        assertNotEquals(valueOne, valueTwo)
+        val original = getValue()
+        assertNotEquals(valueOne, original)
+        assertNotEquals(valueTwo, original)
+        assertNotEquals(valueThree, original)
+        assertNotEquals(valueFour, original)
+        assertNotEquals(valueTwo, valueOne)
+        assertNotEquals(valueThree, valueOne)
+        assertNotEquals(valueFour, valueOne)
+        assertNotEquals(valueThree, valueTwo)
+        assertNotEquals(valueFour, valueTwo)
+        assertNotEquals(valueFour, valueThree)
+    }
+
+    @Test
+    fun addMultipleProviders() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.addProvider(testOne)
+        assertEquals(valueOne, getValue())
+
+        loader.addProvider(testTwo)
+        assertEquals(valueTwo, getValue())
+
+        loader.removeProvider(testOne)
+        assertEquals(valueTwo, getValue())
+
+        loader.removeProvider(testTwo)
+        assertEquals(originalValue, getValue())
     }
 
     @Test
@@ -154,201 +217,429 @@
         val originalValue = getValue()
         val testOne = openOne()
         val testTwo = openTwo()
+        val loader1 = ResourcesLoader()
+        val loader2 = ResourcesLoader()
 
-        addLoader(testOne, testTwo)
-
-        assertEquals(valueTwo, getValue())
-
-        removeLoader(testTwo)
-
+        resources.addLoader(loader1)
+        loader1.addProvider(testOne)
         assertEquals(valueOne, getValue())
 
-        removeLoader(testOne)
+        resources.addLoader(loader2)
+        loader2.addProvider(testTwo)
+        assertEquals(valueTwo, getValue())
 
+        resources.removeLoader(loader1)
+        assertEquals(valueTwo, getValue())
+
+        resources.removeLoader(loader2)
+        assertEquals(originalValue, getValue())
+    }
+
+    @Test
+    fun setMultipleProviders() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.providers = listOf(testOne, testTwo)
+        assertEquals(valueTwo, getValue())
+
+        loader.removeProvider(testTwo)
+        assertEquals(valueOne, getValue())
+
+        loader.providers = Collections.emptyList()
         assertEquals(originalValue, getValue())
     }
 
     @Test
     fun setMultipleLoaders() {
         val originalValue = getValue()
-        val testOne = openOne()
-        val testTwo = openTwo()
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
 
-        setLoaders(testOne, testTwo)
-
+        resources.loaders = listOf(loader1, loader2)
         assertEquals(valueTwo, getValue())
 
-        removeLoader(testTwo)
-
+        resources.removeLoader(loader2)
         assertEquals(valueOne, getValue())
 
-        setLoaders()
-
+        resources.loaders = Collections.emptyList()
         assertEquals(originalValue, getValue())
     }
 
-    @Test
-    fun getLoadersContainsAll() {
+    @Test(expected = UnsupportedOperationException::class)
+    fun getProvidersDoesNotLeakMutability() {
         val testOne = openOne()
-        val testTwo = openTwo()
-
-        addLoader(testOne, testTwo)
-
-        assertThat(getLoaders()).containsAllOf(testOne, testTwo)
+        val loader = ResourcesLoader()
+        val providers = loader.providers
+        providers += testOne
     }
 
-    @Test
+    @Test(expected = UnsupportedOperationException::class)
     fun getLoadersDoesNotLeakMutability() {
+        val loaders = resources.loaders
+        loaders += ResourcesLoader()
+    }
+
+    @Test
+    fun alreadyAddedProviderNoOps() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.addProvider(testOne)
+        loader.addProvider(testTwo)
+        loader.addProvider(testOne)
+
+        assertEquals(2, loader.providers.size)
+        assertEquals(loader.providers[0], testOne)
+        assertEquals(loader.providers[1], testTwo)
+    }
+
+    @Test
+    fun alreadyAddedLoaderNoOps() {
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
+
+        resources.addLoader(loader1)
+        resources.addLoader(loader2)
+        resources.addLoader(loader1)
+
+        assertEquals(2, resources.loaders.size)
+        assertEquals(resources.loaders[0], loader1)
+        assertEquals(resources.loaders[1], loader2)
+    }
+
+    @Test
+    fun repeatedRemoveProviderNoOps() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.addProvider(testOne)
+        loader.addProvider(testTwo)
+
+        loader.removeProvider(testOne)
+        loader.removeProvider(testOne)
+
+        assertEquals(1, loader.providers.size)
+        assertEquals(loader.providers[0], testTwo)
+    }
+
+    @Test
+    fun repeatedRemoveLoaderNoOps() {
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
+
+        resources.loaders = listOf(loader1, loader2)
+        resources.removeLoader(loader1)
+        resources.removeLoader(loader1)
+
+        assertEquals(1, resources.loaders.size)
+        assertEquals(resources.loaders[0], loader2)
+    }
+
+    @Test
+    fun repeatedSetProvider() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.providers = listOf(testOne, testTwo)
+        loader.providers = listOf(testOne, testTwo)
+
+        assertEquals(2, loader.providers.size)
+        assertEquals(loader.providers[0], testOne)
+        assertEquals(loader.providers[1], testTwo)
+    }
+
+    @Test
+    fun repeatedSetLoaders() {
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
+
+        resources.loaders = listOf(loader1, loader2)
+        resources.loaders = listOf(loader1, loader2)
+
+        assertEquals(2, resources.loaders.size)
+        assertEquals(resources.loaders[0], loader1)
+        assertEquals(resources.loaders[1], loader2)
+    }
+
+    @Test
+    fun reorderProviders() {
         val originalValue = getValue()
         val testOne = openOne()
         val testTwo = openTwo()
+        val loader = ResourcesLoader()
 
-        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)
+        resources.addLoader(loader)
+        loader.addProvider(testOne)
+        loader.addProvider(testTwo)
         assertEquals(valueTwo, getValue())
-    }
 
-    @Test
-    fun addToEnd() {
-        val testOne = openOne()
-        val testTwo = openTwo()
-
-        addLoader(testOne)
-
-        assertEquals(valueOne, getValue())
-
-        addLoader(testTwo, 1)
-
+        loader.removeProvider(testOne)
         assertEquals(valueTwo, getValue())
-    }
 
-    @Test(expected = IndexOutOfBoundsException::class)
-    fun addPastEnd() {
-        val testOne = openOne()
-        val testTwo = openTwo()
-
-        addLoader(testOne)
-
+        loader.addProvider(testOne)
         assertEquals(valueOne, getValue())
 
-        addLoader(testTwo, 2)
-    }
-
-    @Test(expected = IndexOutOfBoundsException::class)
-    fun addBeforeFront() {
-        val testOne = openOne()
-        val testTwo = openTwo()
-
-        addLoader(testOne)
-
+        loader.removeProvider(testTwo)
         assertEquals(valueOne, getValue())
 
-        addLoader(testTwo, -1)
+        loader.removeProvider(testOne)
+        assertEquals(originalValue, getValue())
     }
 
     @Test
-    fun reorder() {
+    fun reorderLoaders() {
         val originalValue = getValue()
         val testOne = openOne()
         val testTwo = openTwo()
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(testOne)
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(testTwo)
 
-        addLoader(testOne, testTwo)
-
+        resources.addLoader(loader1)
+        resources.addLoader(loader2)
         assertEquals(valueTwo, getValue())
 
-        removeLoader(testOne)
-
+        resources.removeLoader(loader1)
         assertEquals(valueTwo, getValue())
 
-        addLoader(testOne)
-
+        resources.addLoader(loader1)
         assertEquals(valueOne, getValue())
 
-        removeLoader(testTwo)
-
+        resources.removeLoader(loader2)
         assertEquals(valueOne, getValue())
 
-        removeLoader(testOne)
-
+        resources.removeLoader(loader1)
         assertEquals(originalValue, getValue())
     }
 
+    @Test
+    fun reorderMultipleLoadersAndProviders() {
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val testThree = openThree()
+        val testFour = openFour()
+
+        val loader1 = ResourcesLoader()
+        loader1.providers = listOf(testOne, testTwo)
+
+        val loader2 = ResourcesLoader()
+        loader2.providers = listOf(testThree, testFour)
+
+        resources.loaders = listOf(loader1, loader2)
+        assertEquals(valueFour, getValue())
+
+        resources.loaders = listOf(loader2, loader1)
+        assertEquals(valueTwo, getValue())
+
+        loader1.removeProvider(testTwo)
+        assertEquals(valueOne, getValue())
+
+        loader1.removeProvider(testOne)
+        assertEquals(valueFour, getValue())
+    }
+
+    private fun createContext(context: Context, id: Int): Context {
+        val overrideConfig = Configuration()
+        overrideConfig.orientation = Int.MAX_VALUE - id
+        return context.createConfigurationContext(overrideConfig)
+    }
+
+    @Test
+    fun copyContextLoaders() {
+        val originalValue = getValue()
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
+
+        resources.loaders = listOf(loader1)
+        assertEquals(valueOne, getValue())
+
+        // The child context should include the loaders of the original context.
+        val childContext = createContext(context, 0)
+        assertEquals(valueOne, getValue(childContext))
+
+        // Changing the loaders of the child context should not affect the original context.
+        childContext.resources.loaders = listOf(loader1, loader2)
+        assertEquals(valueOne, getValue())
+        assertEquals(valueTwo, getValue(childContext))
+
+        // Changing the loaders of the original context should not affect the child context.
+        resources.removeLoader(loader1)
+        assertEquals(originalValue, getValue())
+        assertEquals(valueTwo, getValue(childContext))
+
+        // A new context created from the original after an update to the original's loaders should
+        // have the updated loaders.
+        val originalPrime = createContext(context, 2)
+        assertEquals(originalValue, getValue(originalPrime))
+
+        // A new context created from the child context after an update to the child's loaders
+        // should have the updated loaders.
+        val childPrime = createContext(childContext, 1)
+        assertEquals(valueTwo, getValue(childPrime))
+    }
+
+    @Test
+    fun loaderUpdatesAffectContexts() {
+        val originalValue = getValue()
+        val testOne = openOne()
+        val testTwo = openTwo()
+        val loader = ResourcesLoader()
+
+        resources.addLoader(loader)
+        loader.addProvider(testOne)
+        assertEquals(valueOne, getValue())
+
+        val childContext = createContext(context, 0)
+        assertEquals(valueOne, getValue(childContext))
+
+        // Adding a provider to a loader affects all contexts that use the loader.
+        loader.addProvider(testTwo)
+        assertEquals(valueTwo, getValue())
+        assertEquals(valueTwo, getValue(childContext))
+
+        // Changes to the loaders for a context do not affect providers.
+        resources.clearLoaders()
+        assertEquals(originalValue, getValue())
+        assertEquals(valueTwo, getValue(childContext))
+
+        val childContext2 = createContext(context, 1)
+        assertEquals(originalValue, getValue())
+        assertEquals(originalValue, getValue(childContext2))
+
+        childContext2.resources.addLoader(loader)
+        assertEquals(originalValue, getValue())
+        assertEquals(valueTwo, getValue(childContext))
+        assertEquals(valueTwo, getValue(childContext2))
+    }
+
+    @Test
+    fun appLoadersIncludedInActivityContexts() {
+        val loader = ResourcesLoader()
+        loader.addProvider(openOne())
+
+        val applicationContext = context.applicationContext
+        applicationContext.resources.addLoader(loader)
+        assertEquals(valueOne, getValue(applicationContext))
+
+        val activity = mTestActivityRule.launchActivity(Intent())
+        assertEquals(valueOne, getValue(activity))
+
+        applicationContext.resources.clearLoaders()
+    }
+
+    @Test
+    fun loadersApplicationInfoChanged() {
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(openOne())
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(openTwo())
+
+        val applicationContext = context.applicationContext
+        applicationContext.resources.addLoader(loader1)
+        assertEquals(valueOne, getValue(applicationContext))
+
+        var token: IBinder? = null
+        val activity = mTestActivityRule.launchActivity(Intent())
+        mTestActivityRule.runOnUiThread(Runnable {
+            token = activity.activityToken
+            val at = activity.activityThread
+
+            // The activity should have the loaders from the application.
+            assertEquals(valueOne, getValue(applicationContext))
+            assertEquals(valueOne, getValue(activity))
+
+            activity.resources.addLoader(loader2)
+            assertEquals(valueOne, getValue(applicationContext))
+            assertEquals(valueTwo, getValue(activity))
+
+            // Relaunches the activity.
+            at.handleApplicationInfoChanged(activity.applicationInfo)
+        })
+
+        mTestActivityRule.runOnUiThread(Runnable {
+            val activityThread = activity.activityThread
+            val newActivity = activityThread.getActivity(token)
+
+            // The loader added to the activity loaders should not be persisted.
+            assertEquals(valueOne, getValue(applicationContext))
+            assertEquals(valueOne, getValue(newActivity))
+        })
+
+        applicationContext.resources.clearLoaders()
+    }
+
+    @Test
+    fun multipleLoadersHaveSameProviders() {
+        val provider1 = openOne()
+        val loader1 = ResourcesLoader()
+        loader1.addProvider(provider1)
+        val loader2 = ResourcesLoader()
+        loader2.addProvider(provider1)
+        loader2.addProvider(openTwo())
+
+        resources.loaders = listOf(loader1, loader2)
+        assertEquals(valueTwo, getValue())
+
+        resources.loaders = listOf(loader2, loader1)
+        assertEquals(valueOne, getValue())
+
+        assertEquals(2, resources.assets.apkAssets.count { apkAssets -> apkAssets.isForLoader })
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun cannotUseClosedProvider() {
+        val provider = openOne()
+        provider.close()
+        val loader = ResourcesLoader()
+        loader.addProvider(provider)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun cannotCloseUsedProvider() {
+        val provider = openOne()
+        val loader = ResourcesLoader()
+        loader.addProvider(provider)
+        provider.close()
+    }
+
     data class Parameter(
-        val getValue: ResourceLoaderValuesTest.() -> Any,
-        val loaderOne: String,
+        val getValue: Resources.() -> Any,
+        val providerOne: String,
         val valueOne: ResourceLoaderValuesTest.() -> Any,
-        val loaderTwo: String,
+        val providerTwo: String,
         val valueTwo: ResourceLoaderValuesTest.() -> Any,
+        val providerThree: String,
+        val valueThree: ResourceLoaderValuesTest.() -> Any,
+        val providerFour: String,
+        val valueFour: ResourceLoaderValuesTest.() -> Any,
         val dataTypes: List<DataType>
     ) {
         override fun toString(): String {
-            val prefix = loaderOne.commonPrefixWith(loaderTwo)
-            return "$prefix${loaderOne.removePrefix(prefix)}|${loaderTwo.removePrefix(prefix)}"
+            val prefix = providerOne.commonPrefixWith(providerTwo)
+            return "$prefix${providerOne.removePrefix(prefix)}|${providerTwo.removePrefix(prefix)}"
         }
     }
 }
+
+class TestActivity : Activity()
\ No newline at end of file
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
index df2d09a..4e8ee5c 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt
@@ -26,10 +26,6 @@
 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) {
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index a2dab99..df5c9d2 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -72,12 +72,12 @@
     public void testMultipleCallsWithIdenticalParametersCacheReference() {
         Resources resources = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources);
 
         Resources newResources = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(newResources);
         assertSame(resources, newResources);
     }
@@ -86,14 +86,14 @@
     public void testMultipleCallsWithDifferentParametersReturnDifferentReferences() {
         Resources resources = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources);
 
         Configuration overrideConfig = new Configuration();
         overrideConfig.smallestScreenWidthDp = 200;
         Resources newResources = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, overrideConfig,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(newResources);
         assertNotSame(resources, newResources);
     }
@@ -102,12 +102,13 @@
     public void testAddingASplitCreatesANewImpl() {
         Resources resources1 = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         Resources resources2 = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, new String[] { APP_ONE_RES_SPLIT_DIR }, null, null,
-                Display.DEFAULT_DISPLAY, null, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                Display.DEFAULT_DISPLAY, null, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO,null,
+                null);
         assertNotNull(resources2);
 
         assertNotSame(resources1, resources2);
@@ -118,12 +119,12 @@
     public void testUpdateConfigurationUpdatesAllAssetManagers() {
         Resources resources1 = mResourcesManager.getResources(
                 null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         Resources resources2 = mResourcesManager.getResources(
                 null, APP_TWO_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources2);
 
         Binder activity = new Binder();
@@ -131,7 +132,7 @@
         overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE;
         Resources resources3 = mResourcesManager.getResources(
                 activity, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY,
-                overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources3);
 
         // No Resources object should be the same.
@@ -164,13 +165,13 @@
         Binder activity1 = new Binder();
         Resources resources1 = mResourcesManager.getResources(
                 activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         Binder activity2 = new Binder();
         Resources resources2 = mResourcesManager.getResources(
                 activity2, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         // The references themselves should be unique.
@@ -185,7 +186,7 @@
         Binder activity1 = new Binder();
         Resources resources1 = mResourcesManager.createBaseActivityResources(
                 activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         Resources.Theme theme = resources1.newTheme();
@@ -218,7 +219,7 @@
         config1.densityDpi = 280;
         Resources resources1 = mResourcesManager.createBaseActivityResources(
                 activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, config1,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources1);
 
         // Create a Resources based on the Activity.
@@ -226,7 +227,7 @@
         config2.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES;
         Resources resources2 = mResourcesManager.getResources(
                 activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, config2,
-                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
         assertNotNull(resources2);
 
         assertNotSame(resources1, resources2);
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index d211cfd..c6ccd4a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2758,7 +2758,8 @@
                 mDisplayContent.getDisplayId(),
                 null /* overrideConfig */,
                 uiContext.getResources().getCompatibilityInfo(),
-                null /* classLoader */);
+                null /* classLoader */,
+                null /* loaders */);
     }
 
     @VisibleForTesting