Added support for asset loading on Android.

Requires passing the activity's AssetManager object from Java to GNUstep by calling +[NSBundle setJavaAssetManager:withJNIEnv:], which then enables the following features:

- NSBundle main bundle resource paths support for Android assets, e.g. for pathForResource:ofType:, URLForResource:ofType: and related methods.
- NSBundle main bundle info dictionary support if Info.plist exists in Android assets.
- -initWithContentsOfFile: and related methods support for reading Android assets from main bundle in various classes (e.g. NSData, NSDictionary, NSArray, etc.).
- NSFileManager fileExistsAtPath:(isDirectory:) and isReadableFileAtPath: return YES for main bundle asset / asset directory paths.
- NSFileHandle support for reading Android assets from main bundle.
- NSDirectoryEnumerator support for enumerating Android assets from main bundle. Note that recursion into subdirectories is currently not supported by the native Android asset manager API (see https://issuetracker.google.com/issues/37002833).

Also adds support for automatic NSProcessInfo initialization on Android with a fake executable path "/data/data/<app identifier>/exe" (as Android apps don't have a real executable path), and tweaks main bundle initialization to allow that path. Main bundle resource paths are prefixed by "/data/data/<app identifier>/Resources".
This commit is contained in:
Frederik Seiffert 2019-05-09 20:16:18 +02:00
parent ecbecbeabd
commit 3b60b1a8be
10 changed files with 440 additions and 46 deletions

View file

@ -241,6 +241,11 @@ static NSString *library_combo =
nil;
#endif
#ifdef __ANDROID__
static jobject _jassetManager = NULL;
static AAssetManager *_assetManager = NULL;
#endif
/*
* Try to find the absolute path of an executable.
@ -1382,6 +1387,7 @@ _bundle_load_callback(Class theClass, struct objc_category *theCategory)
isNonInstalledTool = YES;
}
#ifndef __ANDROID__ /* don't check suffix on Android's fake executable path */
if (isApplication == YES)
{
s = [path lastPathComponent];
@ -1422,6 +1428,7 @@ _bundle_load_callback(Class theClass, struct objc_category *theCategory)
}
}
}
#endif /* !__ANDROID__ */
if (isApplication == NO)
{
@ -1805,9 +1812,6 @@ IF_NO_GC(
{
NSString *identifier = [self bundleIdentifier];
NSUInteger count;
NSUInteger plen = [_path length];
NSEnumerator *enumerator;
NSString *path;
[load_lock lock];
if (_bundles != nil)
@ -1835,38 +1839,7 @@ IF_NO_GC(
/* Clean up path cache for this bundle.
*/
[pathCacheLock lock];
enumerator = [pathCache keyEnumerator];
while (nil != (path = [enumerator nextObject]))
{
if (YES == [path hasPrefix: _path])
{
if ([path length] == plen)
{
/* Remove the bundle directory path from the cache.
*/
[pathCache removeObjectForKey: path];
}
else
{
unichar c = [path characterAtIndex: plen];
/* if the directory is inside the bundle, remove from cache.
*/
if ('/' == c)
{
[pathCache removeObjectForKey: path];
}
#if defined(_WIN32)
else if ('\\' == c)
{
[pathCache removeObjectForKey: path];
}
#endif
}
}
}
[pathCacheLock unlock];
[self cleanPathCache];
RELEASE(_path);
}
TEST_RELEASE(_frameworkVersion);
@ -2144,6 +2117,48 @@ IF_NO_GC(
addBundlePath(array, contents, primary, subPath, language);
}
}
#ifdef __ANDROID__
// Android: check subdir and localization directly, as AAssetDir and thereby
// NSDirectoryEnumerator doesn't list directories
NSString *originalPrimary = primary;
if (subPath) {
primary = [originalPrimary stringByAppendingPathComponent: subPath];
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, nil, nil);
if (localization) {
primary = [primary stringByAppendingPathComponent:
[localization stringByAppendingPathExtension:@"lproj"]];
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, nil, nil);
} else {
NSString *subPathPrimary = primary;
enumerate = [languages objectEnumerator];
while ((language = [enumerate nextObject])) {
primary = [subPathPrimary stringByAppendingPathComponent:
[localization stringByAppendingPathExtension:@"lproj"]];
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, nil, nil);
}
}
}
if (localization) {
primary = [originalPrimary stringByAppendingPathComponent:
[localization stringByAppendingPathExtension:@"lproj"]];
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, nil, nil);
} else {
enumerate = [languages objectEnumerator];
while ((language = [enumerate nextObject])) {
primary = [originalPrimary stringByAppendingPathComponent:
[localization stringByAppendingPathExtension:@"lproj"]];
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, nil, nil);
}
}
#endif /* __ANDROID__ */
primary = rootPath;
contents = bundle_directory_readable(primary);
addBundlePath(array, contents, primary, subPath, nil);
@ -2525,6 +2540,10 @@ IF_NO_GC(
locale = [[locale lastPathComponent] stringByDeletingPathExtension];
[array addObject: locale];
}
#ifdef __ANDROID__
// TODO: check known languages for existance directly, as AAssetDir and thereby
// NSDirectoryEnumerator doesn't list directories
#endif
return GS_IMMUTABLE(array);
}
@ -3202,5 +3221,126 @@ IF_NO_GC(
return path;
}
- (void)cleanPathCache
{
NSUInteger plen = [_path length];
NSEnumerator *enumerator;
NSString *path;
[pathCacheLock lock];
enumerator = [pathCache keyEnumerator];
while (nil != (path = [enumerator nextObject]))
{
if (YES == [path hasPrefix: _path])
{
if ([path length] == plen)
{
/* Remove the bundle directory path from the cache.
*/
[pathCache removeObjectForKey: path];
}
else
{
unichar c = [path characterAtIndex: plen];
/* if the directory is inside the bundle, remove from cache.
*/
if ('/' == c)
{
[pathCache removeObjectForKey: path];
}
#if defined(_WIN32)
else if ('\\' == c)
{
[pathCache removeObjectForKey: path];
}
#endif
}
}
}
[pathCacheLock unlock];
/* also destroy cached variables depending on bundle paths */
DESTROY(_infoDict);
DESTROY(_localizations);
}
#ifdef __ANDROID__
+ (AAssetManager *)assetManager
{
return _assetManager;
}
+ (void)setJavaAssetManager:(jobject)jassetManager withJNIEnv:(JNIEnv *)env
{
// create global reference to Java asset manager to prevent garbage
// collection
_jassetManager = (*env)->NewGlobalRef(env, jassetManager);
// get native asset manager (may be shared across multiple threads)
_assetManager = AAssetManager_fromJava(env, _jassetManager);
// clean main bundle path cache in case it was accessed before
[_mainBundle cleanPathCache];
}
+ (AAsset *)assetForPath:(NSString *)path
{
AAsset *asset = NULL;
if (_assetManager && _mainBundle)
{
NSString *resourcePath = [_mainBundle resourcePath];
if ([path hasPrefix:resourcePath] && [path length] > [resourcePath length])
{
NSString *assetPath = [path substringFromIndex:[resourcePath length]+1];
asset = AAssetManager_open(_assetManager,
[assetPath fileSystemRepresentation], AASSET_MODE_BUFFER);
}
}
return asset;
}
+ (AAssetDir *)assetDirForPath:(NSString *)path
{
AAssetDir *assetDir = NULL;
if (_assetManager && _mainBundle)
{
NSString *resourcePath = [_mainBundle resourcePath];
if ([path hasPrefix:resourcePath])
{
NSString *assetPath = @"";
if ([path length] > [resourcePath length]) {
assetPath = [path substringFromIndex:[resourcePath length] + 1];
}
assetDir = AAssetManager_openDir(_assetManager,
[assetPath fileSystemRepresentation]);
if (assetDir) {
// AAssetManager_openDir() always returns an object, so we check if
// the directory exists by ensuring it contains a file
BOOL exists = AAssetDir_getNextFileName(assetDir) != NULL;
if (exists) {
AAssetDir_rewind(assetDir);
} else {
AAssetDir_close(assetDir);
assetDir = NULL;
}
}
}
}
return assetDir;
}
#endif /* __ANDROID__ */
@end