Add ResourcesProvider.loadFromDirectory

This API allows a directory to be loaded as if it was a zipped APK.
This is a substitute for the DirectoryAssetProvider API that
currently does not work in the native layer.

Bug: 142716192
Test: atest FrameworksResourceLoaderTests
Change-Id: Ia13e15653e75b421423dd56f9fe89e183ab4cb9a
diff --git a/api/current.txt b/api/current.txt
index 861d6e8..5ae6590 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -12871,6 +12871,7 @@
     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 loadFromDirectory(@NonNull String, @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;
   }
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index b9dad85..078d175 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -86,11 +86,15 @@
     /** The path used to load the apk assets represents an resources.arsc file. */
     private static final int FORMAT_ARSC = 2;
 
+    /** the path used to load the apk assets represents a directory. */
+    private static final int FORMAT_DIR = 3;
+
     // Format types that change how the apk assets are loaded.
     @IntDef(prefix = { "FORMAT_" }, value = {
             FORMAT_APK,
             FORMAT_IDMAP,
             FORMAT_ARSC,
+            FORMAT_DIR
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FormatType {}
@@ -227,6 +231,20 @@
     }
 
     /**
+     * Creates a new ApkAssets instance from the given directory path. The directory should have the
+     * file structure of an APK.
+     *
+     * @param path The path to a directory on disk.
+     * @param flags flags that change the behavior of loaded apk assets
+     * @return a new instance of ApkAssets.
+     * @throws IOException if a disk I/O error or parsing error occurred.
+     */
+    public static @NonNull ApkAssets loadFromDir(@NonNull String path,
+            @PropertyFlags int flags) throws IOException {
+        return new ApkAssets(FORMAT_DIR, path, flags);
+    }
+
+    /**
      * Generates an entirely empty ApkAssets. Needed because the ApkAssets instance and presence
      * is required for a lot of APIs, and it's easier to have a non-null reference rather than
      * tracking a separate identifier.
diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java
index 1d24dda..040b369 100644
--- a/core/java/android/content/res/loader/ResourcesProvider.java
+++ b/core/java/android/content/res/loader/ResourcesProvider.java
@@ -212,6 +212,22 @@
                 null);
     }
 
+    /**
+     * Creates a ResourcesProvider from a directory path.
+     *
+     * File-based resources will be resolved within the directory as if the directory is an APK.
+     *
+     * @param path the path of the directory to treat as an APK
+     * @param assetsProvider the assets provider that overrides the loading of file-based resources
+     */
+    @NonNull
+    public static ResourcesProvider loadFromDirectory(@NonNull String path,
+            @Nullable AssetsProvider assetsProvider) throws IOException {
+        return new ResourcesProvider(ApkAssets.loadFromDir(path, ApkAssets.PROPERTY_LOADER),
+                assetsProvider);
+    }
+
+
     private ResourcesProvider(@NonNull ApkAssets apkAssets,
             @Nullable AssetsProvider assetsProvider) {
         this.mApkAssets = apkAssets;
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index 44f8a31..6acb133 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -48,6 +48,9 @@
 
   // The path used to load the apk assets represents an resources.arsc file.
   FORMAT_ARSC = 2,
+
+  // The path used to load the apk assets represents the a directory.
+  FORMAT_DIRECTORY = 3,
 };
 
 static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t format,
@@ -70,6 +73,9 @@
     case FORMAT_ARSC:
       apk_assets = ApkAssets::LoadTable(path.c_str(), property_flags);
       break;
+    case FORMAT_DIRECTORY:
+      apk_assets = ApkAssets::LoadFromDir(path.c_str(), property_flags);
+      break;
     default:
       const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
       jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
@@ -224,8 +230,8 @@
   }
 
   const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
-  std::unique_ptr<Asset> asset = apk_assets->Open(path_utf8.c_str(),
-                                                  Asset::AccessMode::ACCESS_RANDOM);
+  std::unique_ptr<Asset> asset = apk_assets->GetAssetsProvider()->Open(
+      path_utf8.c_str(),Asset::AccessMode::ACCESS_RANDOM);
   if (asset == nullptr) {
     jniThrowException(env, "java/io/FileNotFoundException", path_utf8.c_str());
     return 0;
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 f88878d..bd2bac5 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
@@ -28,6 +28,8 @@
 import org.junit.Before
 import java.io.Closeable
 import java.io.FileOutputStream
+import java.io.File
+import java.util.zip.ZipInputStream
 
 abstract class ResourceLoaderTestBase {
     protected val PROVIDER_ONE: String = "FrameworksResourceLoaderTests_ProviderOne"
@@ -134,8 +136,42 @@
         DataType.SPLIT -> {
             ResourcesProvider.loadFromSplit(context, "${this}_Split")
         }
+        DataType.DIRECTORY -> {
+            ResourcesProvider.loadFromDirectory(zipToDir("${this}.apk").absolutePath, null)
+        }
     }
 
+
+    /** Extracts an archive-based asset into a directory on disk. */
+    private fun zipToDir(name : String, suffix : String = "") : File {
+        val root = File(context.filesDir, name.split('.')[0] + suffix)
+        if (root.exists()) {
+            return root
+        }
+
+        root.mkdir()
+        ZipInputStream(context.assets.open(name)).use { zis ->
+            while (true) {
+                val entry = zis.nextEntry ?: break
+                val file = File(root, entry.name)
+                if (entry.isDirectory) {
+                    continue
+                }
+
+                file.parentFile.mkdirs()
+                file.outputStream().use { output ->
+                    var b = zis.read()
+                    while (b != -1) {
+                        output.write(b)
+                        b = zis.read()
+                    }
+                }
+            }
+        }
+        return root
+    }
+
+
     /** Loads the asset into a temporary file stored in RAM. */
     private fun loadAssetIntoMemory(asset: AssetFileDescriptor,
                                     leadingGarbageSize: Int = 0,
@@ -175,5 +211,6 @@
         ARSC_RAM_MEMORY,
         ARSC_RAM_MEMORY_OFFSETS,
         SPLIT,
+        DIRECTORY
     }
 }
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 11295f3..c01db0d7 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
@@ -104,7 +104,7 @@
                     listOf(DataType.APK_DISK_FD, DataType.APK_DISK_FD_OFFSETS, DataType.APK_RAM_FD,
                             DataType.APK_RAM_OFFSETS, DataType.ARSC_DISK_FD,
                             DataType.ARSC_DISK_FD_OFFSETS, DataType.ARSC_RAM_MEMORY,
-                            DataType.ARSC_RAM_MEMORY_OFFSETS, DataType.SPLIT)
+                            DataType.ARSC_RAM_MEMORY_OFFSETS, DataType.SPLIT, DataType.DIRECTORY)
             )
 
             // Test resolution of file-based resources and assets with no assets provider.
@@ -164,7 +164,7 @@
                             "layout" to "TableLayout",
                             "drawablePng" to Color.WHITE.toString()),
                     listOf(DataType.APK_DISK_FD, DataType.APK_DISK_FD_OFFSETS, DataType.APK_RAM_FD,
-                            DataType.APK_RAM_OFFSETS, DataType.SPLIT)
+                            DataType.APK_RAM_OFFSETS, DataType.SPLIT, DataType.DIRECTORY)
             )
 
             return parameters.flatMap { parameter ->
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index 946fcc0..f5bf84f 100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -21,6 +21,7 @@
 #include "android-base/errors.h"
 #include "android-base/file.h"
 #include "android-base/logging.h"
+#include "android-base/stringprintf.h"
 #include "android-base/unique_fd.h"
 #include "android-base/utf8.h"
 #include "utils/Compat.h"
@@ -40,28 +41,263 @@
 
 static const std::string kResourcesArsc("resources.arsc");
 
-ApkAssets::ApkAssets(ZipArchiveHandle unmanaged_handle,
+ApkAssets::ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider,
                      std::string path,
                      time_t last_mod_time,
                      package_property_t property_flags)
-    : zip_handle_(unmanaged_handle, ::CloseArchive),
+    : assets_provider_(std::move(assets_provider)),
       path_(std::move(path)),
       last_mod_time_(last_mod_time),
       property_flags_(property_flags) {
 }
 
-std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path,
-                                                 const package_property_t flags) {
-  ::ZipArchiveHandle unmanaged_handle;
-  const int32_t result = ::OpenArchive(path.c_str(), &unmanaged_handle);
-  if (result != 0) {
-    LOG(ERROR) << "Failed to open APK '" << path << "' " << ::ErrorCodeString(result);
-    ::CloseArchive(unmanaged_handle);
-    return {};
+// Provides asset files from a zip file.
+class ZipAssetsProvider : public AssetsProvider {
+ public:
+  ~ZipAssetsProvider() override = default;
+
+  static std::unique_ptr<const AssetsProvider> Create(const std::string& path) {
+    ::ZipArchiveHandle unmanaged_handle;
+    const int32_t result = ::OpenArchive(path.c_str(), &unmanaged_handle);
+    if (result != 0) {
+      LOG(ERROR) << "Failed to open APK '" << path << "' " << ::ErrorCodeString(result);
+      ::CloseArchive(unmanaged_handle);
+      return {};
+    }
+
+    return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider(path, path, unmanaged_handle));
   }
 
-  return LoadImpl(unmanaged_handle,  path, nullptr /*idmap_asset*/, nullptr /*loaded_idmap*/,
-                  flags);
+  static std::unique_ptr<const AssetsProvider> Create(
+      unique_fd fd, const std::string& friendly_name, const off64_t offset = 0,
+      const off64_t length = ApkAssets::kUnknownLength) {
+
+    ::ZipArchiveHandle unmanaged_handle;
+    const int32_t result = (length == ApkAssets::kUnknownLength)
+        ? ::OpenArchiveFd(fd.release(), friendly_name.c_str(), &unmanaged_handle)
+        : ::OpenArchiveFdRange(fd.release(), friendly_name.c_str(), &unmanaged_handle, length,
+                               offset);
+
+    if (result != 0) {
+      LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset
+                 << " and length " << length << ": " << ::ErrorCodeString(result);
+      ::CloseArchive(unmanaged_handle);
+      return {};
+    }
+
+    return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider({}, friendly_name,
+                                                                 unmanaged_handle));
+  }
+
+  // Iterate over all files and directories within the zip. The order of iteration is not
+  // guaranteed to be the same as the order of elements in the central directory but is stable for a
+  // given zip file.
+  bool ForEachFile(const std::string& root_path,
+                   const std::function<void(const StringPiece&, FileType)>& f) const override {
+    // If this is a resource loader from an .arsc, there will be no zip handle
+    if (zip_handle_ == nullptr) {
+      return false;
+    }
+
+    std::string root_path_full = root_path;
+    if (root_path_full.back() != '/') {
+      root_path_full += '/';
+    }
+
+    void* cookie;
+    if (::StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) {
+      return false;
+    }
+
+    std::string name;
+    ::ZipEntry entry{};
+
+    // We need to hold back directories because many paths will contain them and we want to only
+    // surface one.
+    std::set<std::string> dirs{};
+
+    int32_t result;
+    while ((result = ::Next(cookie, &entry, &name)) == 0) {
+      StringPiece full_file_path(name);
+      StringPiece leaf_file_path = full_file_path.substr(root_path_full.size());
+
+      if (!leaf_file_path.empty()) {
+        auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/');
+        if (iter != leaf_file_path.end()) {
+          std::string dir =
+              leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string();
+          dirs.insert(std::move(dir));
+        } else {
+          f(leaf_file_path, kFileTypeRegular);
+        }
+      }
+    }
+    ::EndIteration(cookie);
+
+    // Now present the unique directories.
+    for (const std::string& dir : dirs) {
+      f(dir, kFileTypeDirectory);
+    }
+
+    // -1 is end of iteration, anything else is an error.
+    return result == -1;
+  }
+
+ protected:
+    std::unique_ptr<Asset> OpenInternal(
+        const std::string& path, Asset::AccessMode mode, bool* file_exists) const override {
+    if (file_exists) {
+      *file_exists = false;
+    }
+
+    ::ZipEntry entry;
+    int32_t result = ::FindEntry(zip_handle_.get(), path, &entry);
+    if (result != 0) {
+      return {};
+    }
+
+    if (file_exists) {
+      *file_exists = true;
+    }
+
+    const int fd = ::GetFileDescriptor(zip_handle_.get());
+     const off64_t fd_offset = ::GetFileDescriptorOffset(zip_handle_.get());
+    if (entry.method == kCompressDeflated) {
+      std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
+      if (!map->create(GetPath(), fd, entry.offset + fd_offset, entry.compressed_length,
+                       true /*readOnly*/)) {
+        LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'";
+        return {};
+      }
+
+      std::unique_ptr<Asset> asset =
+          Asset::createFromCompressedMap(std::move(map), entry.uncompressed_length, mode);
+      if (asset == nullptr) {
+        LOG(ERROR) << "Failed to decompress '" << path << "' in APK '" << friendly_name_ << "'";
+        return {};
+      }
+      return asset;
+    } else {
+      std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
+      if (!map->create(GetPath(), fd, entry.offset + fd_offset, entry.uncompressed_length,
+                       true /*readOnly*/)) {
+        LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'";
+        return {};
+      }
+
+      unique_fd ufd;
+      if (!GetPath()) {
+        // If the `path` is not set, create a new `fd` for the new Asset to own in order to create
+        // new file descriptors using Asset::openFileDescriptor. If the path is set, it will be used
+        // to create new file descriptors.
+        ufd = unique_fd(dup(fd));
+        if (!ufd.ok()) {
+          LOG(ERROR) << "Unable to dup fd '" << path << "' in APK '" << friendly_name_ << "'";
+          return {};
+        }
+      }
+
+      std::unique_ptr<Asset> asset = Asset::createFromUncompressedMap(std::move(map),
+          std::move(ufd), mode);
+      if (asset == nullptr) {
+        LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'";
+        return {};
+      }
+      return asset;
+    }
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ZipAssetsProvider);
+
+  explicit ZipAssetsProvider(std::string path,
+                             std::string friendly_name,
+                             ZipArchiveHandle unmanaged_handle)
+                             : zip_handle_(unmanaged_handle, ::CloseArchive),
+                               path_(std::move(path)),
+                               friendly_name_(std::move(friendly_name)) { }
+
+  const char* GetPath() const {
+    return path_.empty() ? nullptr : path_.c_str();
+  }
+
+  using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>;
+  ZipArchivePtr zip_handle_;
+  std::string path_;
+  std::string friendly_name_;
+};
+
+class DirectoryAssetsProvider : AssetsProvider {
+ public:
+  ~DirectoryAssetsProvider() override = default;
+
+  static std::unique_ptr<const AssetsProvider> Create(const std::string& path) {
+    struct stat sb{};
+    const int result = stat(path.c_str(), &sb);
+    if (result == -1) {
+      LOG(ERROR) << "Failed to find directory '" << path << "'.";
+      return nullptr;
+    }
+
+    if (!S_ISDIR(sb.st_mode)) {
+      LOG(ERROR) << "Path '" << path << "' is not a directory.";
+      return nullptr;
+    }
+
+    return std::unique_ptr<AssetsProvider>(new DirectoryAssetsProvider(path));
+  }
+
+ protected:
+  std::unique_ptr<Asset> OpenInternal(
+      const std::string& path, Asset::AccessMode /* mode */, bool* file_exists) const override {
+    const std::string resolved_path = ResolvePath(path);
+    if (file_exists) {
+      struct stat sb{};
+      const int result = stat(resolved_path.c_str(), &sb);
+      *file_exists = result != -1 && S_ISREG(sb.st_mode);
+    }
+
+    return ApkAssets::CreateAssetFromFile(resolved_path);
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(DirectoryAssetsProvider);
+
+  explicit DirectoryAssetsProvider(std::string path) : path_(std::move(path)) { }
+
+  inline std::string ResolvePath(const std::string& path) const {
+    return base::StringPrintf("%s%c%s", path_.c_str(), OS_PATH_SEPARATOR, path.c_str());
+  }
+
+  const std::string path_;
+};
+
+// AssetProvider implementation that does not provide any assets. Used for ApkAssets::LoadEmpty.
+class EmptyAssetsProvider : public AssetsProvider {
+ public:
+  EmptyAssetsProvider() = default;
+  ~EmptyAssetsProvider() override = default;
+
+ protected:
+  std::unique_ptr<Asset> OpenInternal(const std::string& /*path */,
+                                      Asset::AccessMode /* mode */,
+                                      bool* file_exists) const override {
+    if (file_exists) {
+      *file_exists = false;
+    }
+    return nullptr;
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(EmptyAssetsProvider);
+};
+
+std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path,
+                                                 const package_property_t flags) {
+  auto assets = ZipAssetsProvider::Create(path);
+  return (assets) ? LoadImpl(std::move(assets), path, nullptr /*idmap_asset*/,
+                             nullptr /*loaded_idmap*/, flags)
+                  : nullptr;
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadFromFd(unique_fd fd,
@@ -72,33 +308,17 @@
   CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength;
   CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is "
                                                  << kUnknownLength;
-
-  ::ZipArchiveHandle unmanaged_handle;
-  const int32_t result = (length == kUnknownLength)
-      ? ::OpenArchiveFd(fd.release(), friendly_name.c_str(), &unmanaged_handle)
-      : ::OpenArchiveFdRange(fd.release(), friendly_name.c_str(), &unmanaged_handle, length,
-                             offset);
-
-  if (result != 0) {
-    LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset
-               << " and length " << length << ": " << ::ErrorCodeString(result);
-    ::CloseArchive(unmanaged_handle);
-    return {};
-  }
-
-  return LoadImpl(unmanaged_handle, friendly_name, nullptr /*idmap_asset*/,
-                  nullptr /*loaded_idmap*/, flags);
+  auto assets = ZipAssetsProvider::Create(std::move(fd), friendly_name, offset, length);
+  return (assets) ? LoadImpl(std::move(assets), friendly_name, nullptr /*idmap_asset*/,
+                             nullptr /*loaded_idmap*/, flags)
+                  : nullptr;
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadTable(const std::string& path,
                                                       const package_property_t flags) {
   auto resources_asset = CreateAssetFromFile(path);
-  if (!resources_asset) {
-    LOG(ERROR) << "Failed to open ARSC '" << path;
-    return {};
-  }
-
-  return LoadTableImpl(std::move(resources_asset), path, flags);
+  return (resources_asset) ? LoadTableImpl(std::move(resources_asset), path, flags)
+                           : nullptr;
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadTableFromFd(unique_fd fd,
@@ -107,13 +327,8 @@
                                                             const off64_t offset,
                                                             const off64_t length) {
   auto resources_asset = CreateAssetFromFd(std::move(fd), nullptr /* path */, offset, length);
-  if (!resources_asset) {
-    LOG(ERROR) << "Failed to open ARSC '" << friendly_name << "' through FD with offset " << offset
-               << " and length " << length;
-    return {};
-  }
-
-  return LoadTableImpl(std::move(resources_asset), friendly_name, flags);
+  return (resources_asset) ? LoadTableImpl(std::move(resources_asset), friendly_name, flags)
+                           : nullptr;
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path,
@@ -133,24 +348,26 @@
     LOG(ERROR) << "failed to load IDMAP " << idmap_path;
     return {};
   }
-
-
-  ::ZipArchiveHandle unmanaged_handle;
+  
   auto overlay_path = loaded_idmap->OverlayApkPath();
-  const int32_t result = ::OpenArchive(overlay_path.c_str(), &unmanaged_handle);
-  if (result != 0) {
-    LOG(ERROR) << "Failed to open overlay APK '" << overlay_path << "' "
-               << ::ErrorCodeString(result);
-    ::CloseArchive(unmanaged_handle);
-    return {};
-  }
+  auto assets = ZipAssetsProvider::Create(overlay_path);
+  return (assets) ? LoadImpl(std::move(assets), overlay_path, std::move(idmap_asset),
+                             std::move(loaded_idmap), flags | PROPERTY_OVERLAY)
+                  : nullptr;
+}
 
-  return LoadImpl(unmanaged_handle, overlay_path, std::move(idmap_asset), std::move(loaded_idmap),
-                  flags | PROPERTY_OVERLAY);
+std::unique_ptr<const ApkAssets> ApkAssets::LoadFromDir(const std::string& path,
+                                                        const package_property_t flags) {
+  auto assets = DirectoryAssetsProvider::Create(path);
+  return (assets) ? LoadImpl(std::move(assets), path, nullptr /*idmap_asset*/,
+                             nullptr /*loaded_idmap*/, flags)
+                  : nullptr;
 }
 
 std::unique_ptr<const ApkAssets> ApkAssets::LoadEmpty(const package_property_t flags) {
-  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(nullptr, "empty", -1, flags));
+  std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(
+      std::unique_ptr<AssetsProvider>(new EmptyAssetsProvider()), "empty" /* path */,
+                                      -1 /* last_mod-time */, flags));
   loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
   // Need to force a move for mingw32.
   return std::move(loaded_apk);
@@ -196,7 +413,7 @@
                                           Asset::AccessMode::ACCESS_RANDOM);
 }
 
-std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(ZipArchiveHandle unmanaged_handle,
+std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<const AssetsProvider> assets,
                                                      const std::string& path,
                                                      std::unique_ptr<Asset> idmap_asset,
                                                      std::unique_ptr<const LoadedIdmap> idmap,
@@ -205,24 +422,19 @@
 
   // Wrap the handle in a unique_ptr so it gets automatically closed.
   std::unique_ptr<ApkAssets>
-      loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time, property_flags));
+      loaded_apk(new ApkAssets(std::move(assets), path, last_mod_time, property_flags));
 
-  // Find the resource table.
-  ::ZipEntry entry;
-  int32_t result = ::FindEntry(loaded_apk->zip_handle_.get(), kResourcesArsc, &entry);
-  if (result != 0) {
-    // There is no resources.arsc, so create an empty LoadedArsc and return.
+  // Open the resource table via mmap unless it is compressed. This logic is taken care of by Open.
+  bool resources_asset_exists = false;
+  loaded_apk->resources_asset_ = loaded_apk->assets_provider_->Open(
+      kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER, &resources_asset_exists);
+
+  if (!resources_asset_exists) {
     loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
     return std::move(loaded_apk);
   }
 
-  if (entry.method == kCompressDeflated) {
-    ANDROID_LOG(WARNING) << kResourcesArsc << " in APK '" << path << "' is compressed.";
-  }
-
-  // Open the resource table via mmap unless it is compressed. This logic is taken care of by Open.
-  loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
-  if (loaded_apk->resources_asset_ == nullptr) {
+  if (!loaded_apk->resources_asset_) {
     LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << path << "'.";
     return {};
   }
@@ -236,7 +448,7 @@
       loaded_apk->resources_asset_->getLength());
   loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, loaded_apk->loaded_idmap_.get(),
                                               property_flags);
-  if (loaded_apk->loaded_arsc_ == nullptr) {
+  if (!loaded_apk->loaded_arsc_) {
     LOG(ERROR) << "Failed to load '" << kResourcesArsc << "' in APK '" << path << "'.";
     return {};
   }
@@ -251,7 +463,8 @@
   const time_t last_mod_time = getFileModDate(path.c_str());
 
   std::unique_ptr<ApkAssets> loaded_apk(
-      new ApkAssets(nullptr, path, last_mod_time, property_flags));
+      new ApkAssets(std::unique_ptr<AssetsProvider>(new EmptyAssetsProvider()), path, last_mod_time,
+                    property_flags));
   loaded_apk->resources_asset_ = std::move(resources_asset);
 
   const StringPiece data(
@@ -267,111 +480,9 @@
   return std::move(loaded_apk);
 }
 
-std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMode mode) const {
-  // If this is a resource loader from an .arsc, there will be no zip handle
-  if (zip_handle_ == nullptr) {
-    return {};
-  }
-
-  ::ZipEntry entry;
-  int32_t result = ::FindEntry(zip_handle_.get(), path, &entry);
-  if (result != 0) {
-    return {};
-  }
-
-  const int fd = ::GetFileDescriptor(zip_handle_.get());
-  const off64_t fd_offset = ::GetFileDescriptorOffset(zip_handle_.get());
-  if (entry.method == kCompressDeflated) {
-    std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
-    if (!map->create(path_.c_str(), fd, fd_offset + entry.offset, entry.compressed_length,
-                     true /*readOnly*/)) {
-      LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << path_ << "'";
-      return {};
-    }
-
-    std::unique_ptr<Asset> asset =
-        Asset::createFromCompressedMap(std::move(map), entry.uncompressed_length, mode);
-    if (asset == nullptr) {
-      LOG(ERROR) << "Failed to decompress '" << path << "'.";
-      return {};
-    }
-    return asset;
-  } else {
-    std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
-    if (!map->create(path_.c_str(), fd, fd_offset + entry.offset, entry.uncompressed_length,
-                     true /*readOnly*/)) {
-      LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << path_ << "'";
-      return {};
-    }
-
-    // TODO: apks created from file descriptors residing in RAM currently cannot open file
-    //  descriptors to the assets they contain. This is because the Asset::openFileDeescriptor uses
-    //  the zip path on disk to create a new file descriptor. This is fixed in a future change
-    //  in the change topic.
-    std::unique_ptr<Asset> asset = Asset::createFromUncompressedMap(std::move(map),
-        unique_fd(-1) /* fd*/, mode);
-    if (asset == nullptr) {
-      LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << path_ << "'";
-      return {};
-    }
-    return asset;
-  }
-}
-
-bool ApkAssets::ForEachFile(const std::string& root_path,
-                            const std::function<void(const StringPiece&, FileType)>& f) const {
-  // If this is a resource loader from an .arsc, there will be no zip handle
-  if (zip_handle_ == nullptr) {
-    return false;
-  }
-
-  std::string root_path_full = root_path;
-  if (root_path_full.back() != '/') {
-    root_path_full += '/';
-  }
-
-  void* cookie;
-  if (::StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) {
-    return false;
-  }
-
-  std::string name;
-  ::ZipEntry entry;
-
-  // We need to hold back directories because many paths will contain them and we want to only
-  // surface one.
-  std::set<std::string> dirs;
-
-  int32_t result;
-  while ((result = ::Next(cookie, &entry, &name)) == 0) {
-    StringPiece full_file_path(name);
-    StringPiece leaf_file_path = full_file_path.substr(root_path_full.size());
-
-    if (!leaf_file_path.empty()) {
-      auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/');
-      if (iter != leaf_file_path.end()) {
-        std::string dir =
-            leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string();
-        dirs.insert(std::move(dir));
-      } else {
-        f(leaf_file_path, kFileTypeRegular);
-      }
-    }
-  }
-  ::EndIteration(cookie);
-
-  // Now present the unique directories.
-  for (const std::string& dir : dirs) {
-    f(dir, kFileTypeDirectory);
-  }
-
-  // -1 is end of iteration, anything else is an error.
-  return result == -1;
-}
-
 bool ApkAssets::IsUpToDate() const {
   if (IsLoader()) {
-    // Loaders are invalidated by the app, not the system, so assume up to date.
+    // Loaders are invalidated by the app, not the system, so assume they are up to date.
     return true;
   }
 
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 3208662..f20e184 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -463,7 +463,7 @@
       files->add(info);
     };
 
-    if (!apk_assets->ForEachFile(full_path, func)) {
+    if (!apk_assets->GetAssetsProvider()->ForEachFile(full_path, func)) {
       return {};
     }
   }
@@ -487,7 +487,7 @@
       continue;
     }
 
-    std::unique_ptr<Asset> asset = apk_assets_[i]->Open(filename, mode);
+    std::unique_ptr<Asset> asset = apk_assets_[i]->GetAssetsProvider()->Open(filename, mode);
     if (asset) {
       if (out_cookie != nullptr) {
         *out_cookie = i;
@@ -508,7 +508,7 @@
   if (cookie < 0 || static_cast<size_t>(cookie) >= apk_assets_.size()) {
     return {};
   }
-  return apk_assets_[cookie]->Open(filename, mode);
+  return apk_assets_[cookie]->GetAssetsProvider()->Open(filename, mode);
 }
 
 ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h
index 643dc5c..9444768 100644
--- a/libs/androidfw/include/androidfw/ApkAssets.h
+++ b/libs/androidfw/include/androidfw/ApkAssets.h
@@ -35,13 +35,46 @@
 
 class LoadedIdmap;
 
+// Interface for retrieving assets provided by an ApkAssets.
+class AssetsProvider {
+ public:
+  virtual ~AssetsProvider() = default;
+
+  // Opens a file for reading.
+  std::unique_ptr<Asset> Open(const std::string& path,
+                              Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM,
+                              bool* file_exists = nullptr) const {
+    return OpenInternal(path, mode, file_exists);
+  }
+
+  // Iterate over all files and directories provided by the zip. The order of iteration is stable.
+  virtual bool ForEachFile(const std::string& /* path */,
+                           const std::function<void(const StringPiece&, FileType)>& /* f */) const {
+    return true;
+  }
+
+ protected:
+  AssetsProvider() = default;
+
+  virtual std::unique_ptr<Asset> OpenInternal(const std::string& path,
+                                              Asset::AccessMode mode,
+                                              bool* file_exists) const = 0;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(AssetsProvider);
+};
+
+class ZipAssetsProvider;
+
 // Holds an APK.
 class ApkAssets {
+ public:
   // This means the data extends to the end of the file.
   static constexpr off64_t kUnknownLength = -1;
 
- public:
-  // Creates an ApkAssets from the zip path.
+  // Creates an ApkAssets.
+  // If `system` is true, the package is marked as a system package, and allows some functions to
+  // filter out this package when computing what configurations/resources are available.
   static std::unique_ptr<const ApkAssets> Load(const std::string& path,
                                                package_property_t flags = 0U);
 
@@ -75,19 +108,22 @@
   static std::unique_ptr<const ApkAssets> LoadOverlay(const std::string& idmap_path,
                                                       package_property_t flags = 0U);
 
+  // Creates an ApkAssets from the directory path. File-based resources are read within the
+  // directory as if the directory is an APK.
+  static std::unique_ptr<const ApkAssets> LoadFromDir(const std::string& path,
+                                                      package_property_t flags = 0U);
+
   // Creates a totally empty ApkAssets with no resources table and no file entries.
   static std::unique_ptr<const ApkAssets> LoadEmpty(package_property_t flags = 0U);
 
-  std::unique_ptr<Asset> Open(const std::string& path,
-                              Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM) const;
-
-  bool ForEachFile(const std::string& path,
-                   const std::function<void(const StringPiece&, FileType)>& f) const;
-
   inline const std::string& GetPath() const {
     return path_;
   }
 
+  inline const AssetsProvider* GetAssetsProvider() const {
+    return assets_provider_.get();
+  }
+
   // This is never nullptr.
   inline const LoadedArsc* GetLoadedArsc() const {
     return loaded_arsc_.get();
@@ -122,7 +158,7 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(ApkAssets);
 
-  static std::unique_ptr<const ApkAssets> LoadImpl(ZipArchiveHandle unmanaged_handle,
+  static std::unique_ptr<const ApkAssets> LoadImpl(std::unique_ptr<const AssetsProvider> assets,
                                                    const std::string& path,
                                                    std::unique_ptr<Asset> idmap_asset,
                                                    std::unique_ptr<const LoadedIdmap> idmap,
@@ -132,14 +168,12 @@
                                                         const std::string& path,
                                                         package_property_t property_flags);
 
-  ApkAssets(ZipArchiveHandle unmanaged_handle,
+  ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider,
             std::string path,
             time_t last_mod_time,
             package_property_t property_flags);
 
-  using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>;
-
-  ZipArchivePtr zip_handle_;
+  std::unique_ptr<const AssetsProvider> assets_provider_;
   const std::string path_;
   time_t last_mod_time_;
   package_property_t property_flags_ = 0U;
diff --git a/libs/androidfw/include/androidfw/Asset.h b/libs/androidfw/include/androidfw/Asset.h
index 7576174..298509e 100644
--- a/libs/androidfw/include/androidfw/Asset.h
+++ b/libs/androidfw/include/androidfw/Asset.h
@@ -159,6 +159,7 @@
     /* AssetManager needs access to our "create" functions */
     friend class AssetManager;
     friend class ApkAssets;
+    friend class ZipAssetsProvider;
 
     /*
      * Create the asset from a named file on disk.
diff --git a/libs/androidfw/tests/ApkAssets_test.cpp b/libs/androidfw/tests/ApkAssets_test.cpp
index 26bf5ff..ce9e532 100644
--- a/libs/androidfw/tests/ApkAssets_test.cpp
+++ b/libs/androidfw/tests/ApkAssets_test.cpp
@@ -42,7 +42,7 @@
   const LoadedArsc* loaded_arsc = loaded_apk->GetLoadedArsc();
   ASSERT_THAT(loaded_arsc, NotNull());
   ASSERT_THAT(loaded_arsc->GetPackageById(0x7fu), NotNull());
-  ASSERT_THAT(loaded_apk->Open("res/layout/main.xml"), NotNull());
+  ASSERT_THAT(loaded_apk->GetAssetsProvider()->Open("res/layout/main.xml"), NotNull());
 }
 
 TEST(ApkAssetsTest, LoadApkFromFd) {
@@ -57,7 +57,7 @@
   const LoadedArsc* loaded_arsc = loaded_apk->GetLoadedArsc();
   ASSERT_THAT(loaded_arsc, NotNull());
   ASSERT_THAT(loaded_arsc->GetPackageById(0x7fu), NotNull());
-  ASSERT_THAT(loaded_apk->Open("res/layout/main.xml"), NotNull());
+  ASSERT_THAT(loaded_apk->GetAssetsProvider()->Open("res/layout/main.xml"), NotNull());
 }
 
 TEST(ApkAssetsTest, LoadApkAsSharedLibrary) {
@@ -84,9 +84,11 @@
       ApkAssets::Load(GetTestDataPath() + "/basic/basic.apk");
   ASSERT_THAT(loaded_apk, NotNull());
 
-  { ASSERT_THAT(loaded_apk->Open("res/layout/main.xml", Asset::ACCESS_BUFFER), NotNull()); }
+  { ASSERT_THAT(loaded_apk->GetAssetsProvider()->Open("res/layout/main.xml",
+                                                      Asset::ACCESS_BUFFER), NotNull()); }
 
-  { ASSERT_THAT(loaded_apk->Open("res/layout/main.xml", Asset::ACCESS_BUFFER), NotNull()); }
+  { ASSERT_THAT(loaded_apk->GetAssetsProvider()->Open("res/layout/main.xml",
+                                                      Asset::ACCESS_BUFFER), NotNull()); }
 }
 
 TEST(ApkAssetsTest, OpenUncompressedAssetFd) {
@@ -94,7 +96,8 @@
       ApkAssets::Load(GetTestDataPath() + "/basic/basic.apk");
   ASSERT_THAT(loaded_apk, NotNull());
 
-  auto asset = loaded_apk->Open("assets/uncompressed.txt", Asset::ACCESS_UNKNOWN);
+  auto asset = loaded_apk->GetAssetsProvider()->Open("assets/uncompressed.txt",
+                                                     Asset::ACCESS_UNKNOWN);
   ASSERT_THAT(asset, NotNull());
 
   off64_t start, length;
diff --git a/startop/view_compiler/apk_layout_compiler.cc b/startop/view_compiler/apk_layout_compiler.cc
index 09cdbd5..e70c688 100644
--- a/startop/view_compiler/apk_layout_compiler.cc
+++ b/startop/view_compiler/apk_layout_compiler.cc
@@ -100,10 +100,12 @@
       dex_file.MakeClass(StringPrintf("%s.CompiledView", package_name.c_str()))};
   std::vector<dex::MethodBuilder> methods;
 
-  assets->ForEachFile("res/", [&](const android::StringPiece& s, android::FileType) {
+  assets->GetAssetsProvider()->ForEachFile("res/", [&](const android::StringPiece& s,
+                                                       android::FileType) {
     if (s == "layout") {
       auto path = StringPrintf("res/%s/", s.to_string().c_str());
-      assets->ForEachFile(path, [&](const android::StringPiece& layout_file, android::FileType) {
+      assets->GetAssetsProvider()->ForEachFile(path, [&](const android::StringPiece& layout_file,
+                                                         android::FileType) {
         auto layout_path = StringPrintf("%s%s", path.c_str(), layout_file.to_string().c_str());
         android::ApkAssetsCookie cookie = android::kInvalidCookie;
         auto asset = resources.OpenNonAsset(layout_path, android::Asset::ACCESS_RANDOM, &cookie);