/*
* Copyright 2015-present wequick.net
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package net.wequick.small.util;
import android.app.Activity;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.ArrayMap;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipFile;
import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;
/**
* This class consists exclusively of static methods that accelerate reflections.
*/
public class ReflectAccelerator {
// AssetManager.addAssetPath
private static Method sAssetManager_addAssetPath_method;
private static Method sAssetManager_addAssetPaths_method;
// ActivityClientRecord
private static Field sActivityClientRecord_intent_field;
private static Field sActivityClientRecord_activityInfo_field;
private static ArrayMap<Object, WeakReference<Object>> sResourceImpls;
private static Object/*ResourcesImpl*/ sMergedResourcesImpl;
private ReflectAccelerator() { /** cannot be instantiated */ }
private static final class V9_13 {
private static Field sDexClassLoader_mFiles_field;
private static Field sDexClassLoader_mPaths_field;
private static Field sDexClassLoader_mZips_field;
private static Field sDexClassLoader_mDexs_field;
private static Field sPathClassLoader_libraryPathElements_field;
public static boolean expandDexPathList(ClassLoader cl,
String[] dexPaths, DexFile[] dexFiles) {
ZipFile[] zips = null;
try {
/*
* see https://android.googlesource.com/platform/libcore/+/android-2.3_r1/dalvik/src/main/java/dalvik/system/DexClassLoader.java
*/
if (sDexClassLoader_mFiles_field == null) {
sDexClassLoader_mFiles_field = getDeclaredField(cl.getClass(), "mFiles");
sDexClassLoader_mPaths_field = getDeclaredField(cl.getClass(), "mPaths");
sDexClassLoader_mZips_field = getDeclaredField(cl.getClass(), "mZips");
sDexClassLoader_mDexs_field = getDeclaredField(cl.getClass(), "mDexs");
}
if (sDexClassLoader_mFiles_field == null
|| sDexClassLoader_mPaths_field == null
|| sDexClassLoader_mZips_field == null
|| sDexClassLoader_mDexs_field == null) {
return false;
}
int N = dexPaths.length;
Object[] files = new Object[N];
Object[] paths = new Object[N];
zips = new ZipFile[N];
for (int i = 0; i < N; i++) {
String path = dexPaths[i];
files[i] = new File(path);
paths[i] = path;
zips[i] = new ZipFile(path);
}
expandArray(cl, sDexClassLoader_mFiles_field, files, true);
expandArray(cl, sDexClassLoader_mPaths_field, paths, true);
expandArray(cl, sDexClassLoader_mZips_field, zips, true);
expandArray(cl, sDexClassLoader_mDexs_field, dexFiles, true);
} catch (Exception e) {
e.printStackTrace();
if (zips != null) {
for (ZipFile zipFile : zips) {
try {
zipFile.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return false;
}
return true;
}
public static void expandNativeLibraryDirectories(ClassLoader classLoader,
List<File> libPaths) {
if (sPathClassLoader_libraryPathElements_field == null) {
sPathClassLoader_libraryPathElements_field = getDeclaredField(
classLoader.getClass(), "libraryPathElements");
}
List<String> paths = getValue(sPathClassLoader_libraryPathElements_field, classLoader);
if (paths == null) return;
for (File libPath : libPaths) {
paths.add(libPath.getAbsolutePath() + File.separator);
}
}
}
private static class V14_ { // API 14 and upper
// DexPathList
protected static Field sPathListField;
private static Constructor sDexElementConstructor;
private static Class sDexElementClass;
private static Field sDexElementsField;
public static boolean expandDexPathList(ClassLoader cl,
String[] dexPaths, DexFile[] dexFiles) {
try {
int N = dexPaths.length;
Object[] elements = new Object[N];
for (int i = 0; i < N; i++) {
String dexPath = dexPaths[i];
File pkg = new File(dexPath);
DexFile dexFile = dexFiles[i];
elements[i] = makeDexElement(pkg, dexFile);
}
fillDexPathList(cl, elements);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Make dex element
* @see <a href="https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system/DexPathList.java">DexPathList.java</a>
* @param pkg archive android package with any file extensions
* @param dexFile
* @return dalvik.system.DexPathList$Element
*/
private static Object makeDexElement(File pkg, DexFile dexFile) throws Exception {
return makeDexElement(pkg, false, dexFile);
}
protected static Object makeDexElement(File dir) throws Exception {
return makeDexElement(dir, true, null);
}
private static Object makeDexElement(File pkg, boolean isDirectory, DexFile dexFile) throws Exception {
if (sDexElementClass == null) {
sDexElementClass = Class.forName("dalvik.system.DexPathList$Element");
}
if (sDexElementConstructor == null) {
sDexElementConstructor = sDexElementClass.getConstructors()[0];
}
Class<?>[] types = sDexElementConstructor.getParameterTypes();
switch (types.length) {
case 3:
if (types[1].equals(ZipFile.class)) {
// Element(File apk, ZipFile zip, DexFile dex)
ZipFile zip;
try {
zip = new ZipFile(pkg);
} catch (IOException e) {
throw e;
}
try {
return sDexElementConstructor.newInstance(pkg, zip, dexFile);
} catch (Exception e) {
zip.close();
throw e;
}
} else {
// Element(File apk, File zip, DexFile dex)
return sDexElementConstructor.newInstance(pkg, pkg, dexFile);
}
case 4:
default:
// Element(File apk, boolean isDir, File zip, DexFile dex)
if (isDirectory) {
return sDexElementConstructor.newInstance(pkg, true, null, null);
} else {
return sDexElementConstructor.newInstance(pkg, false, pkg, dexFile);
}
}
}
private static void fillDexPathList(ClassLoader cl, Object[] elements)
throws NoSuchFieldException, IllegalAccessException {
if (sPathListField == null) {
sPathListField = getDeclaredField(DexClassLoader.class.getSuperclass(), "pathList");
}
Object pathList = sPathListField.get(cl);
if (sDexElementsField == null) {
sDexElementsField = getDeclaredField(pathList.getClass(), "dexElements");
}
expandArray(pathList, sDexElementsField, elements, true);
}
public static void removeDexPathList(ClassLoader cl, int deleteIndex) {
try {
if (sPathListField == null) {
sPathListField = getDeclaredField(DexClassLoader.class.getSuperclass(), "pathList");
}
Object pathList = sPathListField.get(cl);
if (sDexElementsField == null) {
sDexElementsField = getDeclaredField(pathList.getClass(), "dexElements");
}
sliceArray(pathList, sDexElementsField, deleteIndex);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static final class V9_20 {
private static Method sInstrumentation_execStartActivityV20_method;
public static Instrumentation.ActivityResult execStartActivity(
Instrumentation instrumentation, Context who, IBinder contextThread, IBinder token,
Activity target, Intent intent, int requestCode) {
if (sInstrumentation_execStartActivityV20_method == null) {
Class[] types = new Class[] {Context.class, IBinder.class, IBinder.class,
Activity.class, Intent.class, int.class};
sInstrumentation_execStartActivityV20_method = getMethod(Instrumentation.class,
"execStartActivity", types);
}
if (sInstrumentation_execStartActivityV20_method == null) return null;
return invoke(sInstrumentation_execStartActivityV20_method, instrumentation,
who, contextThread, token, target, intent, requestCode);
}
}
private static final class V21_ {
private static Method sInstrumentation_execStartActivityV21_method;
public static Instrumentation.ActivityResult execStartActivity(
Instrumentation instrumentation, Context who, IBinder contextThread, IBinder token,
Activity target, Intent intent, int requestCode, Bundle options) {
if (sInstrumentation_execStartActivityV21_method == null) {
Class[] types = new Class[] {Context.class, IBinder.class, IBinder.class,
Activity.class, Intent.class, int.class, android.os.Bundle.class};
sInstrumentation_execStartActivityV21_method = getMethod(Instrumentation.class,
"execStartActivity", types);
}
if (sInstrumentation_execStartActivityV21_method == null) return null;
return invoke(sInstrumentation_execStartActivityV21_method, instrumentation,
who, contextThread, token, target, intent, requestCode, options);
}
}
private static class V14_22 extends V14_ {
protected static Field sDexPathList_nativeLibraryDirectories_field;
public static void expandNativeLibraryDirectories(ClassLoader classLoader,
List<File> libPaths) {
if (sPathListField == null) return;
Object pathList = getValue(sPathListField, classLoader);
if (pathList == null) return;
if (sDexPathList_nativeLibraryDirectories_field == null) {
sDexPathList_nativeLibraryDirectories_field = getDeclaredField(
pathList.getClass(), "nativeLibraryDirectories");
if (sDexPathList_nativeLibraryDirectories_field == null) return;
}
try {
// File[] nativeLibraryDirectories
Object[] paths = libPaths.toArray();
expandArray(pathList, sDexPathList_nativeLibraryDirectories_field, paths, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static final class V23_ extends V14_22 {
private static Field sDexPathList_nativeLibraryPathElements_field;
public static void expandNativeLibraryDirectories(ClassLoader classLoader,
List<File> libPaths) {
if (sPathListField == null) return;
Object pathList = getValue(sPathListField, classLoader);
if (pathList == null) return;
if (sDexPathList_nativeLibraryDirectories_field == null) {
sDexPathList_nativeLibraryDirectories_field = getDeclaredField(
pathList.getClass(), "nativeLibraryDirectories");
if (sDexPathList_nativeLibraryDirectories_field == null) return;
}
try {
// List<File> nativeLibraryDirectories
List<File> paths = getValue(sDexPathList_nativeLibraryDirectories_field, pathList);
if (paths == null) return;
paths.addAll(libPaths);
// Element[] nativeLibraryPathElements
if (sDexPathList_nativeLibraryPathElements_field == null) {
sDexPathList_nativeLibraryPathElements_field = getDeclaredField(
pathList.getClass(), "nativeLibraryPathElements");
}
if (sDexPathList_nativeLibraryPathElements_field == null) return;
int N = libPaths.size();
Object[] elements = new Object[N];
for (int i = 0; i < N; i++) {
Object dexElement = makeDexElement(libPaths.get(i));
elements[i] = dexElement;
}
expandArray(pathList, sDexPathList_nativeLibraryPathElements_field, elements, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//______________________________________________________________________________________________
// API
public static AssetManager newAssetManager() {
AssetManager assets;
try {
assets = AssetManager.class.newInstance();
} catch (InstantiationException e1) {
e1.printStackTrace();
return null;
} catch (IllegalAccessException e1) {
e1.printStackTrace();
return null;
}
return assets;
}
public static int addAssetPath(AssetManager assets, String path) {
if (sAssetManager_addAssetPath_method == null) {
sAssetManager_addAssetPath_method = getMethod(AssetManager.class,
"addAssetPath", new Class[]{String.class});
}
if (sAssetManager_addAssetPath_method == null) return 0;
Integer ret = invoke(sAssetManager_addAssetPath_method, assets, path);
if (ret == null) return 0;
return ret;
}
public static int[] addAssetPaths(AssetManager assets, String[] paths) {
if (sAssetManager_addAssetPaths_method == null) {
sAssetManager_addAssetPaths_method = getMethod(AssetManager.class,
"addAssetPaths", new Class[]{String[].class});
}
if (sAssetManager_addAssetPaths_method == null) return null;
return invoke(sAssetManager_addAssetPaths_method, assets, new Object[]{paths});
}
public static void mergeResources(Application app, Object activityThread, String[] assetPaths) {
AssetManager newAssetManager;
if (Build.VERSION.SDK_INT < 24) {
newAssetManager = newAssetManager();
} else {
// On Android 7.0+, this should contains a WebView asset as base. #347
newAssetManager = app.getAssets();
}
addAssetPaths(newAssetManager, assetPaths);
try {
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
Collection<WeakReference<Resources>> references;
if (Build.VERSION.SDK_INT >= 19) {
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap)fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection) mResourceReferences.get(resourcesManager);
}
if (Build.VERSION.SDK_INT >= 24) {
Field fMResourceImpls = resourcesManagerClass.getDeclaredField("mResourceImpls");
fMResourceImpls.setAccessible(true);
sResourceImpls = (ArrayMap)fMResourceImpls.get(resourcesManager);
}
} else {
Field fMActiveResources = activityThread.getClass().getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
HashMap<?, WeakReference<Resources>> map = (HashMap)fMActiveResources.get(activityThread);
references = map.values();
}
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources == null) continue;
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets;
try {
implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
} catch (NoSuchFieldException e) {
// Compat for MiUI 8+
implAssets = resourceImpl.getClass().getSuperclass().getDeclaredField("mAssets");
}
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
if (Build.VERSION.SDK_INT >= 24) {
if (resources == app.getResources()) {
sMergedResourcesImpl = resourceImpl;
}
}
}
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
if (Build.VERSION.SDK_INT >= 21) {
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources == null) continue;
// android.util.Pools$SynchronizedPool<TypedArray>
Field mTypedArrayPool = Resources.class.getDeclaredField("mTypedArrayPool");
mTypedArrayPool.setAccessible(true);
Object typedArrayPool = mTypedArrayPool.get(resources);
// Clear all the pools
Method acquire = typedArrayPool.getClass().getMethod("acquire");
acquire.setAccessible(true);
while (acquire.invoke(typedArrayPool) != null) ;
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
public static void ensureCacheResources() {
if (Build.VERSION.SDK_INT < 24) return;
if (sResourceImpls == null || sMergedResourcesImpl == null) return;
Set<?> resourceKeys = sResourceImpls.keySet();
for (Object resourceKey : resourceKeys) {
WeakReference resourceImpl = (WeakReference)sResourceImpls.get(resourceKey);
if (resourceImpl != null && resourceImpl.get() == null) {
// Sometimes? the weak reference for the key was released by what
// we can not find the cache resources we had merged before.
// And the system will recreate a new one which only build with host resources.
// So we needs to restore the cache. Fix #429.
// FIXME: we'd better to find the way to KEEP the weak reference.
sResourceImpls.put(resourceKey, new WeakReference<Object>(sMergedResourcesImpl));
}
}
}
public static Object getActivityThread(Context context) {
try {
Class activityThread = Class.forName("android.app.ActivityThread");
// ActivityThread.currentActivityThread()
Method m = activityThread.getMethod("currentActivityThread", new Class[0]);
m.setAccessible(true);
Object thread = m.invoke(null, new Object[0]);
if (thread != null) return thread;
// context.@mLoadedApk.@mActivityThread
Field mLoadedApk = context.getClass().getField("mLoadedApk");
mLoadedApk.setAccessible(true);
Object apk = mLoadedApk.get(context);
Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
return mActivityThreadField.get(apk);
} catch (Throwable ignore) {
throw new RuntimeException("Failed to get mActivityThread from context: " + context);
}
}
public static Application getApplication() {
try {
Class activityThread = Class.forName("android.app.ActivityThread");
// ActivityThread.currentActivityThread()
Method m = activityThread.getMethod("currentApplication", new Class[0]);
m.setAccessible(true);
return (Application) m.invoke(null, new Object[0]);
} catch (Throwable ignore) {
throw new RuntimeException("Failed to get current application!");
}
}
public static boolean expandDexPathList(ClassLoader cl, String[] dexPaths, DexFile[] dexFiles) {
if (Build.VERSION.SDK_INT < 14) {
return V9_13.expandDexPathList(cl, dexPaths, dexFiles);
} else {
return V14_.expandDexPathList(cl, dexPaths, dexFiles);
}
}
public static void expandNativeLibraryDirectories(ClassLoader classLoader, List<File> libPath) {
int v = Build.VERSION.SDK_INT;
if (v < 14) {
V9_13.expandNativeLibraryDirectories(classLoader, libPath);
} else if (v < 23) {
V14_22.expandNativeLibraryDirectories(classLoader, libPath);
} else {
V23_.expandNativeLibraryDirectories(classLoader, libPath);
}
}
public static Instrumentation.ActivityResult execStartActivity(
Instrumentation instrumentation,
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, android.os.Bundle options) {
return V21_.execStartActivity(instrumentation,
who, contextThread, token, target, intent, requestCode, options);
}
public static Instrumentation.ActivityResult execStartActivity(
Instrumentation instrumentation,
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
return V9_20.execStartActivity(instrumentation,
who, contextThread, token, target, intent, requestCode);
}
public static boolean relaunchActivity(Activity activity,
Object/*ActivityThread*/ thread,
Object/*IBinder*/ activityToken) {
if (Build.VERSION.SDK_INT >= 11) {
activity.recreate();
return true;
}
try {
Method m = thread.getClass().getDeclaredMethod("getApplicationThread");
m.setAccessible(true);
Object /*ActivityThread$ApplicationThread*/ appThread = m.invoke(thread);
Class[] types = new Class[]{IBinder.class, List.class, List.class,
int.class, boolean.class, Configuration.class};
m = appThread.getClass().getMethod("scheduleRelaunchActivity", types);
m.setAccessible(true);
m.invoke(appThread, activityToken, null, null, 0, false, null);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public static Intent getIntent(Object/*ActivityClientRecord*/ r) {
if (sActivityClientRecord_intent_field == null) {
sActivityClientRecord_intent_field = getDeclaredField(r.getClass(), "intent");
}
return getValue(sActivityClientRecord_intent_field, r);
}
public static ServiceInfo getServiceInfo(Object/*ActivityThread$CreateServiceData*/ data) {
Field f = getDeclaredField(data.getClass(), "info");
return getValue(f, data);
}
public static void setActivityInfo(Object/*ActivityClientRecord*/ r, ActivityInfo ai) {
if (sActivityClientRecord_activityInfo_field == null) {
sActivityClientRecord_activityInfo_field = getDeclaredField(
r.getClass(), "activityInfo");
}
setValue(sActivityClientRecord_activityInfo_field, r, ai);
}
//______________________________________________________________________________________________
// Private
/**
* Add elements to Object[] with reflection
* @see <a href="https://github.com/casidiablo/multidex/blob/publishing/library/src/android/support/multidex/MultiDex.java">MultiDex</a>
* @param target
* @param arrField
* @param extraElements
* @param push true=push to array head, false=append to array tail
* @throws IllegalAccessException
*/
private static void expandArray(Object target, Field arrField,
Object[] extraElements, boolean push)
throws IllegalAccessException {
Object[] original = (Object[]) arrField.get(target);
Object[] combined = (Object[]) Array.newInstance(
original.getClass().getComponentType(), original.length + extraElements.length);
if (push) {
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);
} else {
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
}
arrField.set(target, combined);
}
private static void sliceArray(Object target, Field arrField, int deleteIndex)
throws IllegalAccessException {
Object[] original = (Object[]) arrField.get(target);
if (original.length == 0) return;
Object[] sliced = (Object[]) Array.newInstance(
original.getClass().getComponentType(), original.length - 1);
if (deleteIndex > 0) {
// Copy left elements
System.arraycopy(original, 0, sliced, 0, deleteIndex);
}
int rightCount = original.length - deleteIndex - 1;
if (rightCount > 0) {
// Copy right elements
System.arraycopy(original, deleteIndex + 1, sliced, deleteIndex, rightCount);
}
arrField.set(target, sliced);
}
private static Method getMethod(Class cls, String methodName, Class[] types) {
try {
Method method = cls.getMethod(methodName, types);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
return null;
}
}
private static Field getDeclaredField(Class cls, String fieldName) {
try {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
return null;
}
}
private static <T> T invoke(Method method, Object target, Object... args) {
try {
return (T) method.invoke(target, args);
} catch (Exception e) {
// Ignored
e.printStackTrace();
return null;
}
}
private static <T> T getValue(Field field, Object target) {
if (field == null) {
return null;
}
try {
return (T) field.get(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
private static void setValue(Field field, Object target, Object value) {
if (field == null) {
return;
}
try {
field.set(target, value);
} catch (IllegalAccessException e) {
// Ignored
e.printStackTrace();
}
}
}