| package org.robolectric.shadows; |
| |
| import static org.robolectric.res.android.Errors.NO_ERROR; |
| import static org.robolectric.res.android.Util.ATRACE_NAME; |
| import static org.robolectric.res.android.Util.JNI_TRUE; |
| import static org.robolectric.shadow.api.Shadow.directlyOn; |
| |
| import android.annotation.NonNull; |
| import android.content.res.ApkAssets; |
| import android.content.res.AssetManager; |
| import android.content.res.loader.AssetsProvider; |
| import android.os.Build; |
| |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| import org.robolectric.res.android.Asset; |
| import org.robolectric.res.android.CppApkAssets; |
| import org.robolectric.res.android.Registries; |
| import org.robolectric.res.android.ResXMLTree; |
| import org.robolectric.shadow.api.Shadow; |
| import org.robolectric.shadows.ShadowApkAssets.Picker; |
| import org.robolectric.util.ReflectionHelpers; |
| import org.robolectric.util.ReflectionHelpers.ClassParameter; |
| |
| import java.io.FileDescriptor; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.lang.ref.WeakReference; |
| import java.util.HashMap; |
| import java.util.Objects; |
| |
| |
| // transliterated from |
| // https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/core/jni/android_content_res_ApkAssets.cpp |
| |
| @Implements(value = ApkAssets.class, minSdk = Build.VERSION_CODES.P, |
| shadowPicker = Picker.class, isInAndroidSdk = false) |
| public class ShadowArscApkAssets9 extends ShadowApkAssets { |
| // #define ATRACE_TAG ATRACE_TAG_RESOURCES |
| // |
| // #include "android-base/macros.h" |
| // #include "android-base/stringprintf.h" |
| // #include "android-base/unique_fd.h" |
| // #include "androidfw/ApkAssets.h" |
| // #include "utils/misc.h" |
| // #include "utils/Trace.h" |
| // |
| // #include "core_jni_helpers.h" |
| // #include "jni.h" |
| // #include "nativehelper/ScopedUtfChars.h" |
| // |
| // using ::android::base::unique_fd; |
| // |
| // namespace android { |
| // BEGIN-INTERNAL |
| private static final int PROPERTY_SYSTEM = 1 << 0; |
| private static final int PROPERTY_DYNAMIC = 1 << 1; |
| private static final int PROPERTY_LOADER = 1 << 2; |
| private static final int PROPERTY_OVERLAY = 1 << 3; |
| // END-INTERNAL |
| |
| private static final String FRAMEWORK_APK_PATH = |
| ReflectionHelpers.getStaticField(AssetManager.class, "FRAMEWORK_APK_PATH"); |
| |
| private static final HashMap<Key, WeakReference<ApkAssets>> cachedApkAssets = |
| new HashMap<>(); |
| private static final HashMap<Key, Long> cachedNativePtrs = new HashMap<>(); |
| |
| @RealObject private ApkAssets realApkAssets; |
| |
| long getNativePtr() { |
| return ReflectionHelpers.getField(realApkAssets, "mNativePtr"); |
| } |
| |
| /** |
| * Caching key for {@link ApkAssets}. |
| */ |
| private static class Key { |
| private final FileDescriptor fd; |
| private final String path; |
| private final boolean system; |
| private final boolean load_as_shared_library; |
| private final boolean overlay; |
| |
| public Key(FileDescriptor fd, String path, boolean system, boolean load_as_shared_library, |
| boolean overlay) { |
| this.fd = fd; |
| this.path = path; |
| this.system = system; |
| this.load_as_shared_library = load_as_shared_library; |
| this.overlay = overlay; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| Key key = (Key) o; |
| return system == key.system && |
| load_as_shared_library == key.load_as_shared_library && |
| overlay == key.overlay && |
| Objects.equals(fd, key.fd) && |
| Objects.equals(path, key.path); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(fd, path, system, load_as_shared_library, overlay); |
| } |
| } |
| |
| @FunctionalInterface |
| private interface ApkAssetMaker { |
| ApkAssets call(); |
| } |
| |
| // BEGIN-INTERNAL |
| // Overwrite ApkAssets.java to prevent clearing out the apk assets |
| @Implementation(minSdk = Build.VERSION_CODES.S) |
| protected void close() { |
| } |
| // END-INTERNAL |
| |
| private static ApkAssets getFromCacheOrLoad(Key key, ApkAssetMaker callable) { |
| synchronized (cachedApkAssets) { |
| WeakReference<ApkAssets> cachedRef = cachedApkAssets.get(key); |
| ApkAssets apkAssets; |
| if (cachedRef != null) { |
| apkAssets = cachedRef.get(); |
| if (apkAssets != null) { |
| return apkAssets; |
| } else { |
| cachedApkAssets.remove(key); |
| long nativePtr = cachedNativePtrs.remove(key); |
| Registries.NATIVE_APK_ASSETS_REGISTRY.unregister(nativePtr); |
| } |
| } |
| |
| apkAssets = callable.call(); |
| long nativePtr = ((ShadowArscApkAssets9) Shadow.extract(apkAssets)).getNativePtr(); |
| cachedNativePtrs.put(key, nativePtr); |
| cachedApkAssets.put(key, new WeakReference<>(apkAssets)); |
| return apkAssets; |
| } |
| } |
| |
| @Implementation |
| protected static ApkAssets loadFromPath(@NonNull String path) throws IOException { |
| return getFromCacheOrLoad( |
| new Key(null, path, false, false, false), |
| () -> directlyOn(ApkAssets.class, "loadFromPath", ClassParameter.from(String.class, path))); |
| } |
| |
| /** |
| * Necessary to shadow this method because the framework path is hard-coded. |
| * Called from AssetManager.createSystemAssetsInZygoteLocked() in P+. |
| */ |
| @Implementation |
| protected static ApkAssets loadFromPath(String path, boolean system) |
| throws IOException { |
| System.out.println( |
| "Called loadFromPath(" |
| + path |
| + ", " + system + "); mode=" |
| + (RuntimeEnvironment.useLegacyResources() ? "legacy" : "binary") |
| + " sdk=" + RuntimeEnvironment.getApiLevel()); |
| |
| if (FRAMEWORK_APK_PATH.equals(path)) { |
| path = RuntimeEnvironment.getAndroidFrameworkJarPath(); |
| } |
| |
| String finalPath = path; |
| return getFromCacheOrLoad( |
| new Key(null, path, system, false, false), |
| () -> directlyOn(ApkAssets.class, "loadFromPath", |
| ClassParameter.from(String.class, finalPath), |
| ClassParameter.from(boolean.class, system))); |
| } |
| |
| // BEGIN-INTERNAL |
| @Implementation(minSdk = Build.VERSION_CODES.R) |
| protected static ApkAssets loadFromPath(String path, int flags) |
| throws IOException { |
| boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; |
| |
| if (FRAMEWORK_APK_PATH.equals(path)) { |
| path = RuntimeEnvironment.getAndroidFrameworkJarPath(); |
| } |
| |
| String finalPath = path; |
| return getFromCacheOrLoad( |
| new Key(null, path, system, false, false), |
| () -> directlyOn(ApkAssets.class, "loadFromPath", |
| ClassParameter.from(String.class, finalPath), |
| ClassParameter.from(int.class, flags))); |
| } |
| // END-INTERNAL |
| |
| @Implementation |
| protected static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system, |
| boolean forceSharedLibrary) throws IOException { |
| return getFromCacheOrLoad( |
| new Key(null, path, system, forceSharedLibrary, false), |
| () -> directlyOn(ApkAssets.class, "loadFromPath", |
| ClassParameter.from(String.class, path), |
| ClassParameter.from(boolean.class, system), |
| ClassParameter.from(boolean.class, forceSharedLibrary))); |
| } |
| |
| // BEGIN-INTERNAL |
| @Implementation(minSdk = Build.VERSION_CODES.R) |
| protected static @NonNull ApkAssets loadFromPath(@NonNull String path, int flags, |
| AssetsProvider assets) throws IOException { |
| boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; |
| boolean forceSharedLibrary = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; |
| return getFromCacheOrLoad( |
| new Key(null, path, system, forceSharedLibrary, false), |
| () -> directlyOn(ApkAssets.class, "loadFromPath", |
| ClassParameter.from(String.class, path), |
| ClassParameter.from(int.class, flags), |
| ClassParameter.from(AssetsProvider.class, assets))); |
| } |
| // END-INTERNAL |
| |
| @Implementation |
| protected static ApkAssets loadFromFd(FileDescriptor fd, |
| String friendlyName, boolean system, boolean forceSharedLibrary) |
| throws IOException { |
| return getFromCacheOrLoad( |
| new Key(fd, friendlyName, system, forceSharedLibrary, false), |
| () -> directlyOn(ApkAssets.class, "loadFromFd", |
| ClassParameter.from(FileDescriptor.class, fd), |
| ClassParameter.from(String.class, friendlyName), |
| ClassParameter.from(boolean.class, system), |
| ClassParameter.from(boolean.class, forceSharedLibrary))); |
| } |
| |
| // BEGIN-INTERNAL |
| @Implementation(minSdk = Build.VERSION_CODES.R) |
| protected static ApkAssets loadFromFd(FileDescriptor fd, |
| String friendlyName, int flags, AssetsProvider assets) |
| throws IOException { |
| boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; |
| boolean forceSharedLibrary = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; |
| return getFromCacheOrLoad( |
| new Key(fd, friendlyName, system, forceSharedLibrary, false), |
| () -> directlyOn(ApkAssets.class, "loadFromFd", |
| ClassParameter.from(FileDescriptor.class, fd), |
| ClassParameter.from(String.class, friendlyName), |
| ClassParameter.from(int.class, flags), |
| ClassParameter.from(AssetsProvider.class, assets))); |
| } |
| // END-INTERNAL |
| |
| @Implementation |
| protected static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system) |
| throws IOException { |
| throw new UnsupportedOperationException(); |
| // return getFromCacheOrLoad( |
| // new Key(fd, friendlyName, system, forceSharedLibrary, false), |
| // () -> directlyOn(ApkAssets.class, "loadFromPath", |
| // ClassParameter.from(FileDescriptor.class, fd), |
| // ClassParameter.from(String.class, friendlyName), |
| // ClassParameter.from(boolean.class, system), |
| // ClassParameter.from(boolean.class, forceSharedLibrary))); |
| } |
| |
| // static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, |
| // jboolean force_shared_lib, jboolean overlay) { |
| @Implementation(maxSdk = Build.VERSION_CODES.Q) |
| protected static long nativeLoad(String java_path, boolean system, |
| boolean force_shared_lib, boolean overlay) throws IOException { |
| String path = java_path; |
| if (path == null) { |
| return 0; |
| } |
| |
| ATRACE_NAME(String.format("LoadApkAssets(%s)", path)); |
| |
| CppApkAssets apk_assets; |
| try { |
| if (overlay) { |
| apk_assets = CppApkAssets.LoadOverlay(path, system); |
| } else if (force_shared_lib) { |
| apk_assets = |
| CppApkAssets.LoadAsSharedLibrary(path, system); |
| } else { |
| apk_assets = CppApkAssets.Load(path, system); |
| } |
| } catch (OutOfMemoryError e) { |
| OutOfMemoryError outOfMemoryError = new OutOfMemoryError("Failed to load " + path); |
| outOfMemoryError.initCause(e); |
| throw outOfMemoryError; |
| } |
| |
| if (apk_assets == null) { |
| String error_msg = String.format("Failed to load asset path %s", path); |
| throw new IOException(error_msg); |
| } |
| return Registries.NATIVE_APK_ASSETS_REGISTRY.register(apk_assets); |
| } |
| |
| // BEGIN-INTERNAL |
| // static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t format, |
| // jstring java_path, const jint property_flags, jobject assets_provider) |
| @Implementation(minSdk = Build.VERSION_CODES.R) |
| protected static long nativeLoad(int format, String java_path, |
| int flags, AssetsProvider asset) throws IOException { |
| String path = java_path; |
| if (path == null) { |
| return 0; |
| } |
| |
| ATRACE_NAME(String.format("LoadApkAssets(%s)", path)); |
| |
| boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; |
| boolean overlay = (flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY; |
| boolean force_shared_lib = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; |
| CppApkAssets apk_assets; |
| try { |
| if (overlay) { |
| apk_assets = CppApkAssets.LoadOverlay(path, system); |
| } else if (force_shared_lib) { |
| apk_assets = |
| CppApkAssets.LoadAsSharedLibrary(path, system); |
| } else { |
| apk_assets = CppApkAssets.Load(path, system); |
| } |
| } catch (OutOfMemoryError e) { |
| OutOfMemoryError outOfMemoryError = new OutOfMemoryError("Failed to load " + path); |
| outOfMemoryError.initCause(e); |
| throw outOfMemoryError; |
| } |
| |
| if (apk_assets == null) { |
| String error_msg = String.format("Failed to load asset path %s", path); |
| throw new IOException(error_msg); |
| } |
| return Registries.NATIVE_APK_ASSETS_REGISTRY.register(apk_assets); |
| } |
| // END-INTERNAL |
| |
| // static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor, |
| // jstring friendly_name, jboolean system, jboolean |
| // force_shared_lib) { |
| @Implementation(maxSdk = Build.VERSION_CODES.Q) |
| protected static long nativeLoadFromFd(FileDescriptor file_descriptor, |
| String friendly_name, boolean system, boolean force_shared_lib) { |
| String friendly_name_utf8 = friendly_name; |
| if (friendly_name_utf8 == null) { |
| return 0; |
| } |
| |
| throw new UnsupportedOperationException(); |
| // ATRACE_NAME(String.format("LoadApkAssetsFd(%s)", friendly_name_utf8)); |
| // |
| // int fd = jniGetFDFromFileDescriptor(env, file_descriptor); |
| // if (fd < 0) { |
| // throw new IllegalArgumentException("Bad FileDescriptor"); |
| // } |
| // |
| // unique_fd dup_fd(.dup(fd)); |
| // if (dup_fd < 0) { |
| // throw new IOException(errno); |
| // return 0; |
| // } |
| // |
| // ApkAssets apk_assets = ApkAssets.LoadFromFd(std.move(dup_fd), |
| // friendly_name_utf8, |
| // system, force_shared_lib); |
| // if (apk_assets == null) { |
| // String error_msg = String.format("Failed to load asset path %s from fd %d", |
| // friendly_name_utf8, dup_fd.get()); |
| // throw new IOException(error_msg); |
| // return 0; |
| // } |
| // return ShadowArscAssetManager9.NATIVE_APK_ASSETS_REGISTRY.getNativeObjectId(apk_assets); |
| } |
| |
| // BEGIN-INTERNAL |
| // static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t format, |
| // jobject file_descriptor, jstring friendly_name, |
| // const jint property_flags, jobject assets_provider) { |
| @Implementation(minSdk = Build.VERSION_CODES.R) |
| protected static long nativeLoadFromFd(int format, FileDescriptor fd, |
| String friendlyName, int flags, AssetsProvider asset) { |
| String friendly_name_utf8 = friendlyName; |
| if (friendly_name_utf8 == null) { |
| return 0; |
| } |
| |
| throw new UnsupportedOperationException(); |
| // ATRACE_NAME(String.format("LoadApkAssetsFd(%s)", friendly_name_utf8)); |
| // |
| // int fd = jniGetFDFromFileDescriptor(env, file_descriptor); |
| // if (fd < 0) { |
| // throw new IllegalArgumentException("Bad FileDescriptor"); |
| // } |
| // |
| // unique_fd dup_fd(.dup(fd)); |
| // if (dup_fd < 0) { |
| // throw new IOException(errno); |
| // return 0; |
| // } |
| // |
| // ApkAssets apk_assets = ApkAssets.LoadFromFd(std.move(dup_fd), |
| // friendly_name_utf8, |
| // system, force_shared_lib); |
| // if (apk_assets == null) { |
| // String error_msg = String.format("Failed to load asset path %s from fd %d", |
| // friendly_name_utf8, dup_fd.get()); |
| // throw new IOException(error_msg); |
| // return 0; |
| // } |
| // return ShadowArscAssetManager9.NATIVE_APK_ASSETS_REGISTRY.getNativeObjectId(apk_assets); |
| } |
| // END-INTERNAL |
| |
| // static jstring NativeGetAssetPath(JNIEnv* env, jclass /*clazz*/, jlong ptr) { |
| @Implementation |
| protected static String nativeGetAssetPath(long ptr) { |
| CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr); |
| return apk_assets.GetPath(); |
| } |
| |
| // static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) { |
| @Implementation |
| protected static long nativeGetStringBlock(long ptr) { |
| CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr); |
| return apk_assets.GetLoadedArsc().GetStringPool().getNativePtr(); |
| } |
| |
| // static jboolean NativeIsUpToDate(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) { |
| @Implementation |
| protected static boolean nativeIsUpToDate(long ptr) { |
| CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr); |
| // (void)apk_assets; |
| return JNI_TRUE; |
| } |
| |
| // static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { |
| @Implementation |
| protected static long nativeOpenXml(long ptr, String file_name) throws FileNotFoundException { |
| String path_utf8 = file_name; |
| if (path_utf8 == null) { |
| return 0; |
| } |
| |
| CppApkAssets apk_assets = |
| Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr); |
| Asset asset = apk_assets.Open(path_utf8, |
| Asset.AccessMode.ACCESS_RANDOM); |
| if (asset == null) { |
| throw new FileNotFoundException(path_utf8); |
| } |
| |
| // DynamicRefTable is only needed when looking up resource references. Opening an XML file |
| // directly from an ApkAssets has no notion of proper resource references. |
| ResXMLTree xml_tree = new ResXMLTree(null); // util.make_unique<ResXMLTree>(nullptr /*dynamicRefTable*/); |
| int err = xml_tree.setTo(asset.getBuffer(true), (int) asset.getLength(), true); |
| // asset.reset(); |
| |
| if (err != NO_ERROR) { |
| throw new FileNotFoundException("Corrupt XML binary file"); |
| } |
| return Registries.NATIVE_RES_XML_TREES.register(xml_tree); // reinterpret_cast<jlong>(xml_tree.release()); |
| } |
| |
| // // JNI registration. |
| // static const JNINativeMethod gApkAssetsMethods[] = { |
| // {"nativeLoad", "(Ljava/lang/String;ZZZ)J", (void*)NativeLoad}, |
| // {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZ)J", |
| // (void*)NativeLoadFromFd}, |
| // {"nativeDestroy", "(J)V", (void*)NativeDestroy}, |
| // {"nativeGetAssetPath", "(J)Ljava/lang/String;", (void*)NativeGetAssetPath}, |
| // {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, |
| // {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, |
| // {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, |
| // }; |
| // |
| // int register_android_content_res_ApkAssets(JNIEnv* env) { |
| // return RegisterMethodsOrDie(env, "android/content/res/ApkAssets", gApkAssetsMethods, |
| // arraysize(gApkAssetsMethods)); |
| // } |
| // |
| // } // namespace android |
| } |