package com.tns;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import com.tns.bindings.AnnotationDescriptor;
import com.tns.bindings.ProxyGenerator;
import com.tns.bindings.desc.ClassDescriptor;
import com.tns.bindings.desc.reflection.ClassInfo;
import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;
public class DexFactory {
private static final char CLASS_NAME_LOCATION_SEPARATOR = '_';
private final Logger logger;
private final File dexDir;
private final File odexDir;
private final String dexThumb;
private final ClassLoader classLoader;
private ProxyGenerator proxyGenerator;
private HashMap<String, Class<?>> injectedDexClasses = new HashMap<String, Class<?>>();
public DexFactory(Logger logger, ClassLoader classLoader, File dexBaseDir, String dexThumb) {
this.logger = logger;
this.classLoader = classLoader;
this.dexDir = dexBaseDir;
this.dexThumb = dexThumb;
this.odexDir = new File(this.dexDir, "odex");
this.proxyGenerator = new ProxyGenerator(this.dexDir.getAbsolutePath());
ProxyGenerator.IsLogEnabled = logger.isEnabled();
if (!dexDir.exists()) {
dexDir.mkdirs();
}
if (!odexDir.exists()) {
odexDir.mkdir();
}
this.updateDexThumbAndPurgeCache();
this.proxyGenerator.setProxyThumb(this.dexThumb);
}
static long totalGenTime = 0;
static long totalMultiDexTime = 0;
static long totalLoadDexTime = 0;
public Class<?> resolveClass(String name, String className, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) throws ClassNotFoundException, IOException {
String fullClassName = className.replace("$", "_");
if (!isInterface) {
fullClassName += CLASS_NAME_LOCATION_SEPARATOR + name;
}
// try to get pre-generated binding classes
try {
if (logger.isEnabled()) {
logger.write("getting pre-generated proxy class with name: " + fullClassName.replace("-", "_"));
}
Class<?> pregeneratedClass = classLoader.loadClass(fullClassName.replace("-", "_"));
if (logger.isEnabled()) {
logger.write("Pre-generated class found: " + fullClassName.replace("-", "_"));
}
return pregeneratedClass;
} catch (Exception e) {
}
//
Class<?> existingClass = this.injectedDexClasses.get(fullClassName);
if (existingClass != null) {
return existingClass;
}
String classToProxy = this.getClassToProxyName(className);
String dexFilePath = classToProxy;
if (!isInterface) {
dexFilePath += CLASS_NAME_LOCATION_SEPARATOR + name;
}
File dexFile = this.getDexFile(dexFilePath);
// generate dex file
if (dexFile == null) {
long startGenTime = System.nanoTime();
if (logger.isEnabled()) {
logger.write("generating proxy in place");
}
dexFilePath = this.generateDex(name, classToProxy, methodOverrides, implementedInterfaces, isInterface);
dexFile = new File(dexFilePath);
long stopGenTime = System.nanoTime();
totalGenTime += stopGenTime - startGenTime;
if (logger.isEnabled()) {
logger.write("Finished inplace gen took: " + (stopGenTime - startGenTime) / 1000000.0 + "ms");
logger.write("TotalGenTime: " + totalGenTime / 1000000.0 + "ms");
}
}
// creates jar file from already generated dex file
String jarFilePath = dexFile.getPath().replace(".dex", ".jar");
File jarFile = new File(jarFilePath);
if (!jarFile.exists()) {
FileOutputStream jarFileStream = new FileOutputStream(jarFile);
ZipOutputStream out = new ZipOutputStream(jarFileStream);
out.putNextEntry(new ZipEntry("classes.dex"));
byte[] dexData = new byte[(int) dexFile.length()];
FileInputStream fi = new FileInputStream(dexFile);
fi.read(dexData, 0, dexData.length);
fi.close();
out.write(dexData);
out.closeEntry();
out.close();
}
//
Class<?> result = null;
DexFile df = null;
try {
// use DexFile instead of DexClassLoader to allow class loading
// within the default class loader
// Note: According to the official documentation, DexFile should not
// be directly used.
// However, this is the only viable way to get our dynamic classes
// loaded within the system class loader
df = DexFile.loadDex(jarFilePath, new File(this.odexDir, fullClassName).getAbsolutePath(), 0);
result = df.loadClass(fullClassName, classLoader);
} catch (IOException e) {
e.printStackTrace();
// fall back to DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader);
result = dexClassLoader.loadClass(fullClassName);
}
this.injectedDexClasses.put(fullClassName, result);
return result;
}
public Class<?> findClass(String className) throws ClassNotFoundException {
String canonicalName = className.replace('/', '.');
if (logger.isEnabled()) {
logger.write(canonicalName);
}
Class<?> existingClass = this.injectedDexClasses.get(canonicalName);
if (existingClass != null) {
return existingClass;
}
return classLoader.loadClass(canonicalName);
}
public static String strJoin(String[] array, String separator) {
if (array == null) {
return "";
}
StringBuilder sbStr = new StringBuilder();
for (int i = 0, il = array.length; i < il; i++) {
if (i > 0) {
sbStr.append(separator);
}
sbStr.append(array[i]);
}
return sbStr.toString();
}
private String getClassToProxyName(String className) throws InvalidClassException {
String classToProxy = className;
if (className.startsWith("com.tns.gen.")) {
classToProxy = className.substring(12);
}
if (classToProxy.startsWith("com.tns.gen.")) {
throw new InvalidClassException("Can't generate proxy of proxy");
}
return classToProxy;
}
private File getDexFile(String className) throws InvalidClassException {
String classToProxyFile = className.replace("$", "_");
if (this.dexThumb != null) {
classToProxyFile += "-" + this.dexThumb;
}
String dexFilePath = dexDir + "/" + classToProxyFile + ".dex";
File dexFile = new File(dexFilePath);
if (dexFile.exists()) {
if (logger.isEnabled()) {
logger.write("Looking for proxy file: " + dexFilePath + " Result: proxy file Found. ClassName: " + className);
}
return dexFile;
}
if (logger.isEnabled()) {
logger.write("Looking for proxy file: " + dexFilePath + " Result: NOT Found. Proxy Gen needed. ClassName: " + className);
}
return null;
}
private String generateDex(String proxyName, String className, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) throws ClassNotFoundException, IOException {
Class<?> classToProxy = Class.forName(className);
HashSet<String> methodOverridesSet = null;
HashSet<ClassDescriptor> implementedInterfacesSet = new HashSet<ClassDescriptor>();
if (methodOverrides != null) {
methodOverridesSet = new HashSet<String>();
for (int i = 0; i < methodOverrides.length; i++) {
String methodOverride = methodOverrides[i];
methodOverridesSet.add(methodOverride);
}
}
if (implementedInterfaces.length > 0) {
for (int j = 0; j < implementedInterfaces.length; j++) {
if (!implementedInterfaces[j].isEmpty()) {
implementedInterfacesSet.add(new ClassInfo(Class.forName(implementedInterfaces[j])));
}
}
}
AnnotationDescriptor[] annotations = null;
return proxyGenerator.generateProxy(proxyName, new ClassInfo(classToProxy) , methodOverridesSet, implementedInterfacesSet, isInterface, annotations);
}
private void updateDexThumbAndPurgeCache() {
if (this.dexThumb == null) {
throw new RuntimeException("Error generating proxy thumb 1");
}
String oldDexThumb = this.getCachedProxyThumb(this.dexDir);
if (this.dexThumb.equals(oldDexThumb)) {
return;
}
if (oldDexThumb != null) {
this.purgeDexesByThumb(oldDexThumb, this.dexDir);
this.purgeDexesByThumb(oldDexThumb, this.odexDir);
} else {
// purge all dex files if no thumb file is found. This is crucial for CLI livesync
purgeAllProxies();
}
this.saveNewDexThumb(this.dexThumb, this.dexDir);
}
public void purgeAllProxies() {
this.purgeDexesByThumb(null, this.dexDir);
this.purgeDexesByThumb(null, this.odexDir);
}
private void saveNewDexThumb(String newDexThumb, File dexDir) {
File cachedThumbFile = new File(dexDir, "proxyThumb");
try {
FileOutputStream out = new FileOutputStream(cachedThumbFile, false);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
try {
writer.write(newDexThumb);
writer.newLine();
writer.flush();
} finally {
writer.close();
out.close();
}
} catch (FileNotFoundException e) {
logger.write("Error while writting current proxy thumb");
e.printStackTrace();
} catch (IOException e) {
logger.write("Error while writting current proxy thumb");
e.printStackTrace();
}
}
private void purgeDexesByThumb(String cachedDexThumb, File pathToPurge) {
if (!pathToPurge.exists()) {
return;
}
if (!pathToPurge.isDirectory()) {
logger.write("Purge proxies path not a directory. Path: " + pathToPurge);
throw new RuntimeException("Purge path not a directory");
}
String[] children = pathToPurge.list();
for (int i = 0; i < children.length; i++) {
String filename = children[i];
File purgeCandidate = new File(pathToPurge, filename);
if (purgeCandidate.isDirectory()) {
this.purgeDexesByThumb(cachedDexThumb, purgeCandidate);
} else {
if (cachedDexThumb != null && !filename.contains(cachedDexThumb)) {
continue;
}
if (!purgeCandidate.delete()) {
logger.write("Error purging cached proxy file: " + purgeCandidate.getAbsolutePath());
}
}
}
}
private String getCachedProxyThumb(File proxyDir) {
try {
File cachedThumbFile = new File(proxyDir, "proxyThumb");
if (cachedThumbFile.exists()) {
FileInputStream in = new FileInputStream(cachedThumbFile);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String cachedThumb = reader.readLine();
reader.close();
in.close();
return cachedThumb;
}
} catch (FileNotFoundException e) {
logger.write("Error while getting current proxy thumb");
e.printStackTrace();
} catch (IOException e) {
logger.write("Error while getting current proxy thumb");
e.printStackTrace();
}
return null;
}
}