Overriding Bundle.module for loading resources from Android assets

A Foundation Bundle expects resources to be present as a plain file on disk. For example, the call Bundle.module.url(forResource: "file", withExtension: "json") will return something like:

file:/…apppath…/Contents/Resources/ModuleName.bundle/Contents/Resources/file.json

This works fine for Darwin apps (iOS, macOS, etc.), because their contents are expanded on disk. This is not the case for Android apps: an app.apk file – the zip of an app's code and resources – is not expanded on disk when the app is installed, but instead remains zipped up. Android expects app resources to be stored in an assets/ folder of the .apk archive and accessed via an "Asset Manager" API.

This presents a problem for code that relies on Foundation conventions to access resources. In Skip, we handle this by adding resources to a module-specific folder in the /assets/ section of the archive and intercepting calls to Bundle.module.url(…) to return a custom URL like:

asset:/module-name/Resources/file.json

We handle the "asset" protocol by registering a custom URL protocol handler in both Java (using java.net.URL.setURLStreamHandlerFactory) and Foundation (using URLProtocol.registerClass) to process these requests with the Java SDK's android.content.res.AssetManager or the NDK's AAssetManager, respectively.

This works for single-shot loading of data (e.g., Data(contentsOf: Bundle.module.url(forResource: "file.json")), and in theory could even work to provide random-access to large assets with their support for asset-aware file descriptors (with AssetManager.openFd and AAsset_openFileDescriptor64).

All this works fairly well, but the way we have to implement it is ugly: the client code needs to import SkipFuse, which allows us to typealias Bundle = AndroidAssetBundle so we can override Bundle.module and Bundle.main. In order to make this work transparently, we would need a way to hook into the Bundle loading system. A couple ideas we have for this:

  1. Use @_dynamicReplacement to intercept calls to Bundle.url and the like. This would require that Android's Foundation module be built with -enable-dynamic-replacement, which may have performance implications.
  2. Add some SPI capabilities to Bundle like Bundle.registerBundleProvider(handler: (String) -> Bundle?) and alter SwiftPM's generated resource_bundle_accessor.swift to first try Bundle.bundleProvider?(handler: "BundleName") to see if a custom provider has been registered.

Does anyone have any other ideas?

2 Likes