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:
- Use
@_dynamicReplacement
to intercept calls toBundle.url
and the like. This would require that Android's Foundation module be built with-enable-dynamic-replacement
, which may have performance implications. - Add some SPI capabilities to Bundle like
Bundle.registerBundleProvider(handler: (String) -> Bundle?)
and alter SwiftPM's generatedresource_bundle_accessor.swift
to first tryBundle.bundleProvider?(handler: "BundleName")
to see if a custom provider has been registered.
Does anyone have any other ideas?