mov patch to as project
This commit is contained in:
parent
cb7b40cbd3
commit
c7eff6fe1f
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_7
|
||||
targetCompatibility = JavaVersion.VERSION_1_7
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,287 @@
|
|||
package com.storm.wind.xpatch;
|
||||
|
||||
import com.storm.wind.xpatch.base.BaseCommand;
|
||||
import com.storm.wind.xpatch.task.ApkModifyTask;
|
||||
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
|
||||
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
|
||||
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
|
||||
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
import com.storm.wind.xpatch.util.ManifestParser;
|
||||
import com.wind.meditor.core.FileProcesser;
|
||||
import com.wind.meditor.property.AttributeItem;
|
||||
import com.wind.meditor.property.ModificationProperty;
|
||||
import com.wind.meditor.utils.NodeValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.apache.commons.io.FileUtils.copyFile;
|
||||
|
||||
public class MainCommand extends BaseCommand {
|
||||
|
||||
private String apkPath;
|
||||
|
||||
private String unzipApkFilePath;
|
||||
|
||||
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
|
||||
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
|
||||
private String output; // 输出的apk文件的目录以及名称
|
||||
|
||||
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
|
||||
private boolean forceOverwrite = false;
|
||||
|
||||
@Opt(opt = "pn", longOpt = "proxyname", description = "special proxy app name with full dot path", argName = "proxy app name")
|
||||
private String proxyname = "com.wind.xposed.entry.MMPApplication";
|
||||
|
||||
@Opt(opt = "c", longOpt = "crach", hasArg = false,
|
||||
description = "disable craching the apk's signature.")
|
||||
private boolean disableCrackSignature = false;
|
||||
|
||||
// 使用dex文件中插入代码的方式修改apk,而不是默认的修改Manifest中Application name的方式
|
||||
@Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute")
|
||||
private boolean dexModificationMode = false;
|
||||
|
||||
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
|
||||
private String newPackageName;
|
||||
|
||||
@Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " +
|
||||
"set 0 to make the app debuggable = false", argName = "0 or 1")
|
||||
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
|
||||
|
||||
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
|
||||
argName = "new-version-code")
|
||||
private int versionCode;
|
||||
|
||||
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
|
||||
argName = "new-version-name")
|
||||
private String versionName;
|
||||
|
||||
// 原来apk中dex文件的数量
|
||||
private int dexFileCount = 0;
|
||||
|
||||
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
|
||||
|
||||
private List<Runnable> mXpatchTasks = new ArrayList<>();
|
||||
|
||||
public static void main(String... args) {
|
||||
new MainCommand().doMain(args);
|
||||
}
|
||||
|
||||
private void fuckIfFail(boolean b) {
|
||||
if (!b) {
|
||||
throw new IllegalStateException("wtf", new Throwable("DUMPBT"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCommandLine() throws IOException {
|
||||
if (remainingArgs.length != 1) {
|
||||
if (remainingArgs.length == 0) {
|
||||
System.out.println("Please choose one apk file you want to process. ");
|
||||
}
|
||||
if (remainingArgs.length > 1) {
|
||||
System.out.println("This tool can only used with one apk file.");
|
||||
}
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
apkPath = remainingArgs[0];
|
||||
|
||||
File srcApkFile = new File(apkPath);
|
||||
|
||||
if (!srcApkFile.exists()) {
|
||||
System.out.println("The source apk file not exsit, please choose another one. " +
|
||||
"current apk file is = " + apkPath);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDir = new File(".").getAbsolutePath();
|
||||
System.out.println("currentDir: " + currentDir);
|
||||
System.out.println("apkPath: " + apkPath);
|
||||
|
||||
if (output == null || output.length() == 0) {
|
||||
output = getBaseName(apkPath) + "-xposed-signed.apk";
|
||||
}
|
||||
|
||||
File outputFile = new File(output);
|
||||
if (outputFile.exists() && !forceOverwrite) {
|
||||
System.err.println(output + " exists, use --force to overwrite");
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
String outputApkFileParentPath = outputFile.getParent();
|
||||
if (outputApkFileParentPath == null) {
|
||||
String absPath = outputFile.getAbsolutePath();
|
||||
int index = absPath.lastIndexOf(File.separatorChar);
|
||||
outputApkFileParentPath = absPath.substring(0, index);
|
||||
}
|
||||
|
||||
System.out.println("output apk path: " + output);
|
||||
System.out.println("disableCrackSignature: " + disableCrackSignature);
|
||||
|
||||
String apkFileName = getBaseName(srcApkFile);
|
||||
|
||||
String tempFilePath = outputApkFileParentPath + File.separator +
|
||||
currentTimeStr() + "-tmp" + File.separator;
|
||||
|
||||
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
|
||||
|
||||
System.out.println("outputApkFileParentPath: " + outputApkFileParentPath);
|
||||
System.out.println("unzipApkFilePath = " + unzipApkFilePath);
|
||||
|
||||
if (!disableCrackSignature) {
|
||||
// save the apk original signature info, to support crach signature.
|
||||
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
FileUtils.decompressZip(apkPath, unzipApkFilePath);
|
||||
|
||||
System.out.println("decompress apk cost time: " + (System.currentTimeMillis() - currentTime) + "ms");
|
||||
|
||||
// Get the dex count in the apk zip file
|
||||
dexFileCount = findDexFileCount(unzipApkFilePath);
|
||||
|
||||
System.out.println("dexFileCount: " + dexFileCount);
|
||||
|
||||
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
|
||||
|
||||
currentTime = System.currentTimeMillis();
|
||||
|
||||
// parse the app main application full name from the manifest file
|
||||
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
|
||||
String applicationName = null;
|
||||
if (pair != null && pair.applicationName != null) {
|
||||
applicationName = pair.applicationName;
|
||||
}
|
||||
|
||||
System.out.println("Get application name cost time: " + (System.currentTimeMillis() - currentTime) + "ms");
|
||||
System.out.println("Get the application name: " + applicationName);
|
||||
|
||||
// modify manifest
|
||||
File manifestFile = new File(manifestFilePath);
|
||||
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
|
||||
File manifestFileNew = new File(manifestFilePathNew);
|
||||
fuckIfFail(manifestFile.renameTo(manifestFileNew));
|
||||
|
||||
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
|
||||
|
||||
// new manifest may not exist
|
||||
if (manifestFile.exists() && manifestFile.length() > 0) {
|
||||
fuckIfFail(manifestFileNew.delete());
|
||||
} else {
|
||||
fuckIfFail(manifestFileNew.renameTo(manifestFile));
|
||||
}
|
||||
|
||||
// save original main application name to asset file
|
||||
if (isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
|
||||
}
|
||||
|
||||
// modify the apk dex file to make xposed can run in it
|
||||
if (dexModificationMode && isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new ApkModifyTask(true, true, unzipApkFilePath, applicationName,
|
||||
dexFileCount));
|
||||
}
|
||||
|
||||
// copy xposed so and dex files into the unzipped apk
|
||||
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath));
|
||||
|
||||
// compress all files into an apk and then sign it.
|
||||
mXpatchTasks.add(new BuildAndSignApkTask(true, unzipApkFilePath, output));
|
||||
|
||||
// copy origin apk to assets
|
||||
// convenient to bypass some check like CRC
|
||||
copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin"));
|
||||
|
||||
// excute these tasks
|
||||
for (Runnable executor : mXpatchTasks) {
|
||||
currentTime = System.currentTimeMillis();
|
||||
executor.run();
|
||||
|
||||
System.out.println(executor.getClass().getSimpleName() + " cost time: "
|
||||
+ (System.currentTimeMillis() - currentTime) + "ms");
|
||||
}
|
||||
|
||||
System.out.println("Output APK: " + output);
|
||||
}
|
||||
|
||||
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
|
||||
ModificationProperty property = new ModificationProperty();
|
||||
boolean modifyEnabled = false;
|
||||
if (isNotEmpty(newPackageName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
|
||||
}
|
||||
|
||||
if (versionCode > 0) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
|
||||
}
|
||||
|
||||
if (isNotEmpty(versionName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
|
||||
}
|
||||
|
||||
if (debuggable >= 0) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
|
||||
}
|
||||
|
||||
property.addApplicationAttribute(new AttributeItem("extractNativeLibs", true));
|
||||
|
||||
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, proxyname));
|
||||
}
|
||||
|
||||
if (modifyEnabled) {
|
||||
FileProcesser.processManifestFile(filePath, dstFilePath, property);
|
||||
}
|
||||
}
|
||||
|
||||
private int findDexFileCount(String unzipApkFilePath) {
|
||||
File zipfileRoot = new File(unzipApkFilePath);
|
||||
if (!zipfileRoot.exists()) {
|
||||
return 0;
|
||||
}
|
||||
File[] childFiles = zipfileRoot.listFiles();
|
||||
if (childFiles == null || childFiles.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (File file : childFiles) {
|
||||
String fileName = file.getName();
|
||||
if (Pattern.matches("classes.*\\.dex", fileName)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Use the current timestamp as the name of the build file
|
||||
private String currentTimeStr() {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
private String[] getXposedModules(String modules) {
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return modules.split(File.pathSeparator);
|
||||
}
|
||||
|
||||
private static boolean isNotEmpty(String str) {
|
||||
return str != null && !str.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
package com.storm.wind.xpatch.base;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public abstract class BaseCommand {
|
||||
|
||||
private String onlineHelp;
|
||||
|
||||
protected Map<String, Option> optMap = new HashMap<String, Option>();
|
||||
|
||||
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
|
||||
private boolean printHelp = false;
|
||||
|
||||
protected String[] remainingArgs;
|
||||
protected String[] orginalArgs;
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.FIELD })
|
||||
static public @interface Opt {
|
||||
String argName() default "";
|
||||
|
||||
String description() default "";
|
||||
|
||||
boolean hasArg() default true;
|
||||
|
||||
String longOpt() default "";
|
||||
|
||||
String opt() default "";
|
||||
|
||||
boolean required() default false;
|
||||
}
|
||||
|
||||
static protected class Option implements Comparable<Option> {
|
||||
public String argName = "arg";
|
||||
public String description;
|
||||
public Field field;
|
||||
public boolean hasArg = true;
|
||||
public String longOpt;
|
||||
public String opt;
|
||||
public boolean required = false;
|
||||
|
||||
@Override
|
||||
public int compareTo(Option o) {
|
||||
int result = s(this.opt, o.opt);
|
||||
if (result == 0) {
|
||||
result = s(this.longOpt, o.longOpt);
|
||||
if (result == 0) {
|
||||
result = s(this.argName, o.argName);
|
||||
if (result == 0) {
|
||||
result = s(this.description, o.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int s(String a, String b) {
|
||||
if (a != null && b != null) {
|
||||
return a.compareTo(b);
|
||||
} else if (a != null) {
|
||||
return 1;
|
||||
} else if (b != null) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getOptAndLongOpt() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean havePrev = false;
|
||||
if (opt != null && opt.length() > 0) {
|
||||
sb.append("-").append(opt);
|
||||
havePrev = true;
|
||||
}
|
||||
if (longOpt != null && longOpt.length() > 0) {
|
||||
if (havePrev) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("--").append(longOpt);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.TYPE })
|
||||
static public @interface Syntax {
|
||||
|
||||
String cmd();
|
||||
|
||||
String desc() default "";
|
||||
|
||||
String onlineHelp() default "";
|
||||
|
||||
String syntax() default "";
|
||||
}
|
||||
|
||||
public void doMain(String... args) {
|
||||
try {
|
||||
initOptions();
|
||||
parseSetArgs(args);
|
||||
doCommandLine();
|
||||
} catch (HelpException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.length() > 0) {
|
||||
System.err.println("ERROR: " + msg);
|
||||
}
|
||||
usage();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void doCommandLine() throws Exception;
|
||||
|
||||
protected String getVersionString() {
|
||||
return getClass().getPackage().getImplementationVersion();
|
||||
}
|
||||
|
||||
protected void initOptions() {
|
||||
initOptionFromClass(this.getClass());
|
||||
}
|
||||
|
||||
protected void initOptionFromClass(Class<?> clz) {
|
||||
if (clz == null) {
|
||||
return;
|
||||
} else {
|
||||
initOptionFromClass(clz.getSuperclass());
|
||||
}
|
||||
|
||||
Syntax syntax = clz.getAnnotation(Syntax.class);
|
||||
if (syntax != null) {
|
||||
this.onlineHelp = syntax.onlineHelp();
|
||||
}
|
||||
|
||||
Field[] fs = clz.getDeclaredFields();
|
||||
for (Field f : fs) {
|
||||
Opt opt = f.getAnnotation(Opt.class);
|
||||
if (opt != null) {
|
||||
f.setAccessible(true);
|
||||
Option option = new Option();
|
||||
option.field = f;
|
||||
option.description = opt.description();
|
||||
option.hasArg = opt.hasArg();
|
||||
option.required = opt.required();
|
||||
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
|
||||
option.longOpt = fromCamel(f.getName());
|
||||
if (f.getType().equals(boolean.class)) {
|
||||
option.hasArg=false;
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
continue;
|
||||
}
|
||||
if (!opt.hasArg()) {
|
||||
if (!f.getType().equals(boolean.class)) {
|
||||
throw new RuntimeException("the type of " + f
|
||||
+ " must be boolean, as it is declared as no args");
|
||||
}
|
||||
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
boolean haveLongOpt = false;
|
||||
if (!"".equals(opt.longOpt())) {
|
||||
option.longOpt = opt.longOpt();
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
haveLongOpt = true;
|
||||
}
|
||||
if (!"".equals(opt.argName())) {
|
||||
option.argName = opt.argName();
|
||||
}
|
||||
if (!"".equals(opt.opt())) {
|
||||
option.opt = opt.opt();
|
||||
checkConflict(option, "-" + option.opt);
|
||||
} else {
|
||||
if (!haveLongOpt) {
|
||||
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkConflict(Option option, String key) {
|
||||
if (optMap.containsKey(key)) {
|
||||
Option preOption = optMap.get(key);
|
||||
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
|
||||
preOption.field.toString(), option.field
|
||||
));
|
||||
}
|
||||
optMap.put(key, option);
|
||||
}
|
||||
|
||||
private static String fromCamel(String name) {
|
||||
if (name.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] charArray = name.toCharArray();
|
||||
sb.append(Character.toLowerCase(charArray[0]));
|
||||
for (int i = 1; i < charArray.length; i++) {
|
||||
char c = charArray[i];
|
||||
if (Character.isUpperCase(c)) {
|
||||
sb.append("-").append(Character.toLowerCase(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
|
||||
this.orginalArgs = args;
|
||||
List<String> remainsOptions = new ArrayList<String>();
|
||||
Set<Option> requiredOpts = collectRequriedOptions(optMap);
|
||||
Option needArgOpt = null;
|
||||
for (String s : args) {
|
||||
if (needArgOpt != null) {
|
||||
Field field = needArgOpt.field;
|
||||
Class clazz = field.getType();
|
||||
if (clazz.equals(List.class)) {
|
||||
try {
|
||||
List<Object> object = ((List<Object>) field.get(this));
|
||||
|
||||
// 获取List对象的泛型类型
|
||||
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
|
||||
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
|
||||
Class typeClazz = (Class) listActualTypeArguments[0];
|
||||
object.add(convert(s, typeClazz));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
field.set(this, convert(s, clazz));
|
||||
}
|
||||
needArgOpt = null;
|
||||
} else if (s.startsWith("-")) {// its a short or long option
|
||||
Option opt = optMap.get(s);
|
||||
requiredOpts.remove(opt);
|
||||
if (opt == null) {
|
||||
System.err.println("ERROR: Unrecognized option: " + s);
|
||||
throw new HelpException();
|
||||
} else {
|
||||
if (opt.hasArg) {
|
||||
needArgOpt = opt;
|
||||
} else {
|
||||
opt.field.set(this, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remainsOptions.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (needArgOpt != null) {
|
||||
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
|
||||
throw new HelpException();
|
||||
}
|
||||
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
|
||||
if (this.printHelp) {
|
||||
throw new HelpException();
|
||||
}
|
||||
if (!requiredOpts.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ERROR: Options: ");
|
||||
boolean first = true;
|
||||
for (Option option : requiredOpts) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(" and ");
|
||||
}
|
||||
sb.append(option.getOptAndLongOpt());
|
||||
}
|
||||
sb.append(" is required");
|
||||
System.err.println(sb.toString());
|
||||
throw new HelpException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected Object convert(String value, Class type) {
|
||||
if (type.equals(String.class)) {
|
||||
return value;
|
||||
}
|
||||
if (type.equals(int.class) || type.equals(Integer.class)) {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
if (type.equals(long.class) || type.equals(Long.class)) {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
if (type.equals(float.class) || type.equals(Float.class)) {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
if (type.equals(double.class) || type.equals(Double.class)) {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
if (type.equals(File.class)) {
|
||||
return new File(value);
|
||||
}
|
||||
if (type.equals(Path.class)) {
|
||||
return new File(value).toPath();
|
||||
}
|
||||
try {
|
||||
type.asSubclass(Enum.class);
|
||||
return Enum.valueOf(type, value);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
throw new RuntimeException("can't convert [" + value + "] to type " + type);
|
||||
}
|
||||
|
||||
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
|
||||
Set<Option> options = new HashSet<Option>();
|
||||
for (Map.Entry<String, Option> e : optMap.entrySet()) {
|
||||
Option option = e.getValue();
|
||||
if (option.required) {
|
||||
options.add(option);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class HelpException extends RuntimeException {
|
||||
|
||||
public HelpException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HelpException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void usage() {
|
||||
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
|
||||
|
||||
final int maxLength = 80;
|
||||
final int maxPaLength = 40;
|
||||
// out.println(this.cmdName + " -- " + desc);
|
||||
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
|
||||
if (this.optMap.size() > 0) {
|
||||
out.println("options:");
|
||||
}
|
||||
// [PART.A.........][Part.B
|
||||
// .-a,--aa.<arg>...desc1
|
||||
// .................desc2
|
||||
// .-b,--bb
|
||||
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
|
||||
int palength = -1;
|
||||
for (Option option : options) {
|
||||
int pa = 4 + option.getOptAndLongOpt().length();
|
||||
if (option.hasArg) {
|
||||
pa += 3 + option.argName.length();
|
||||
}
|
||||
if (pa < maxPaLength) {
|
||||
if (pa > palength) {
|
||||
palength = pa;
|
||||
}
|
||||
}
|
||||
}
|
||||
int pblength = maxLength - palength;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Option option : options) {
|
||||
sb.setLength(0);
|
||||
sb.append(" ").append(option.getOptAndLongOpt());
|
||||
if (option.hasArg) {
|
||||
sb.append(" <").append(option.argName).append(">");
|
||||
}
|
||||
String desc = option.description;
|
||||
if (desc == null || desc.length() == 0) {// no description
|
||||
out.println(sb);
|
||||
} else {
|
||||
for (int i = palength - sb.length(); i > 0; i--) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (sb.length() > maxPaLength) {// to huge part A
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
int nextStart = 0;
|
||||
while (nextStart < desc.length()) {
|
||||
if (desc.length() - nextStart < pblength) {// can put in one line
|
||||
sb.append(desc.substring(nextStart));
|
||||
out.println(sb);
|
||||
nextStart = desc.length();
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(desc.substring(nextStart, nextStart + pblength));
|
||||
out.println(sb);
|
||||
nextStart += pblength;
|
||||
sb.setLength(0);
|
||||
if (nextStart < desc.length()) {
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
String ver = getVersionString();
|
||||
if (ver != null && !"".equals(ver)) {
|
||||
out.println("version: " + ver);
|
||||
}
|
||||
if (onlineHelp != null && !"".equals(onlineHelp)) {
|
||||
if (onlineHelp.length() + "online help: ".length() > maxLength) {
|
||||
out.println("online help: ");
|
||||
out.println(onlineHelp);
|
||||
} else {
|
||||
out.println("online help: " + onlineHelp);
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public static String getBaseName(String fn) {
|
||||
int x = fn.lastIndexOf('.');
|
||||
return x >= 0 ? fn.substring(0, x) : fn;
|
||||
}
|
||||
|
||||
// 获取文件不包含后缀的名称
|
||||
public static String getBaseName(File fn) {
|
||||
return getBaseName(fn.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.storm.wind.xpatch.task;
|
||||
|
||||
import com.googlecode.dex2jar.tools.Dex2jarCmd;
|
||||
import com.googlecode.dex2jar.tools.Jar2Dex;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class ApkModifyTask implements Runnable {
|
||||
|
||||
private static final String JAR_FILE_NAME = "output-jar.jar";
|
||||
|
||||
private String unzipApkFilePath;
|
||||
private boolean keepJarFile;
|
||||
private boolean showAllLogs;
|
||||
private String applicationName;
|
||||
|
||||
private int dexFileCount;
|
||||
|
||||
public ApkModifyTask(boolean showAllLogs, boolean keepJarFile, String unzipApkFilePath, String applicationName, int
|
||||
dexFileCount) {
|
||||
this.showAllLogs = showAllLogs;
|
||||
this.unzipApkFilePath = unzipApkFilePath;
|
||||
this.keepJarFile = keepJarFile;
|
||||
this.applicationName = applicationName;
|
||||
this.dexFileCount = dexFileCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
File unzipApkFile = new File(unzipApkFilePath);
|
||||
|
||||
String jarOutputPath = unzipApkFile.getParent() + File.separator + JAR_FILE_NAME;
|
||||
|
||||
// classes.dex
|
||||
String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" the application class is in this dex file = " + targetDexFileName);
|
||||
}
|
||||
|
||||
String dexOutputPath = unzipApkFilePath + targetDexFileName;
|
||||
File dexFile = new File(dexOutputPath);
|
||||
if (dexFile.exists()) {
|
||||
dexFile.delete();
|
||||
}
|
||||
// 将jar转换为dex文件
|
||||
jar2DexCmd(jarOutputPath, dexOutputPath);
|
||||
|
||||
// 删除掉jar文件
|
||||
File jarFile = new File(jarOutputPath);
|
||||
if (!keepJarFile && jarFile.exists()) {
|
||||
jarFile.delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String dumpJarFile(int dexFileCount, String dexFilePath, String jarOutputPath, String applicationName) {
|
||||
ArrayList<String> dexFileList = createClassesDotDexFileList(dexFileCount);
|
||||
// String jarOutputPath = dexFilePath + JAR_FILE_NAME;
|
||||
for (String dexFileName : dexFileList) {
|
||||
String filePath = dexFilePath + dexFileName;
|
||||
// 执行dex2jar命令,修改源代码
|
||||
boolean isApplicationClassFound = dex2JarCmd(filePath, jarOutputPath, applicationName);
|
||||
// 找到了目标应用主application的包名,说明代码注入成功,则返回当前dex文件
|
||||
if (isApplicationClassFound) {
|
||||
return dexFileName;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean dex2JarCmd(String dexPath, String jarOutputPath, String applicationName) {
|
||||
Dex2jarCmd cmd = new Dex2jarCmd();
|
||||
String[] args = new String[]{
|
||||
dexPath,
|
||||
"-o",
|
||||
jarOutputPath,
|
||||
"-app",
|
||||
applicationName,
|
||||
"--force"
|
||||
};
|
||||
cmd.doMain(args);
|
||||
|
||||
boolean isApplicationClassFounded = cmd.isApplicationClassFounded();
|
||||
if (showAllLogs) {
|
||||
System.out.println("isApplicationClassFounded -> " + isApplicationClassFounded + "the dexPath is " +
|
||||
dexPath);
|
||||
}
|
||||
return isApplicationClassFounded;
|
||||
}
|
||||
|
||||
private void jar2DexCmd(String jarFilePath, String dexOutPath) {
|
||||
Jar2Dex cmd = new Jar2Dex();
|
||||
String[] args = new String[]{
|
||||
jarFilePath,
|
||||
"-o",
|
||||
dexOutPath
|
||||
};
|
||||
cmd.doMain(args);
|
||||
}
|
||||
|
||||
// 列出目录下所有dex文件,classes.dex,classes2.dex,classes3.dex .....
|
||||
private ArrayList<String> createClassesDotDexFileList(int dexFileCount) {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
for (int i = 0; i < dexFileCount; i++) {
|
||||
if (i == 0) {
|
||||
list.add("classes.dex");
|
||||
} else {
|
||||
list.add("classes" + (i + 1) + ".dex");
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.storm.wind.xpatch.task;
|
||||
|
||||
import com.android.apksigner.ApkSignerTool;
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
import com.storm.wind.xpatch.util.ShellCmdUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class BuildAndSignApkTask implements Runnable {
|
||||
|
||||
private boolean keepUnsignedApkFile;
|
||||
|
||||
private String signedApkPath;
|
||||
|
||||
private String unzipApkFilePath;
|
||||
|
||||
public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String signedApkPath) {
|
||||
this.keepUnsignedApkFile = keepUnsignedApkFile;
|
||||
this.unzipApkFilePath = unzipApkFilePath;
|
||||
this.signedApkPath = signedApkPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
File unzipApkFile = new File(unzipApkFilePath);
|
||||
|
||||
// 将文件压缩到当前apk文件的上一级目录上
|
||||
String unsignedApkPath = unzipApkFile.getParent() + File.separator + "unsigned.apk";
|
||||
FileUtils.compressToZip(unzipApkFilePath, unsignedApkPath);
|
||||
|
||||
// 将签名文件复制从assets目录下复制出来
|
||||
String keyStoreFilePath = unzipApkFile.getParent() + File.separator + "keystore";
|
||||
|
||||
File keyStoreFile = new File(keyStoreFilePath);
|
||||
// assets/keystore分隔符不能使用File.separator,否则在windows上抛出IOException !!!
|
||||
String keyStoreAssetPath;
|
||||
if (isAndroid()) {
|
||||
// BKS-V1 类型
|
||||
keyStoreAssetPath = "assets/android.keystore";
|
||||
} else {
|
||||
// BKS 类型
|
||||
keyStoreAssetPath = "assets/keystore";
|
||||
}
|
||||
|
||||
FileUtils.copyFileFromJar(keyStoreAssetPath, keyStoreFilePath);
|
||||
|
||||
boolean signResult = signApk(unsignedApkPath, keyStoreFilePath, signedApkPath);
|
||||
|
||||
File unsignedApkFile = new File(unsignedApkPath);
|
||||
File signedApkFile = new File(signedApkPath);
|
||||
// delete unsigned apk file
|
||||
if (!keepUnsignedApkFile && unsignedApkFile.exists() && signedApkFile.exists() && signResult) {
|
||||
unsignedApkFile.delete();
|
||||
}
|
||||
|
||||
// delete the keystore file
|
||||
if (keyStoreFile.exists()) {
|
||||
keyStoreFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean signApk(String apkPath, String keyStorePath, String signedApkPath) {
|
||||
if (signApkUsingAndroidApksigner(apkPath, keyStorePath, signedApkPath, "123456")) {
|
||||
return true;
|
||||
}
|
||||
if (isAndroid()) {
|
||||
System.out.println(" Sign apk failed, please sign it yourself.");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
long time = System.currentTimeMillis();
|
||||
File keystoreFile = new File(keyStorePath);
|
||||
if (keystoreFile.exists()) {
|
||||
StringBuilder signCmd;
|
||||
signCmd = new StringBuilder("jarsigner ");
|
||||
signCmd.append(" -keystore ")
|
||||
.append(keyStorePath)
|
||||
.append(" -storepass ")
|
||||
.append("123456")
|
||||
.append(" -signedjar ")
|
||||
.append(" " + signedApkPath + " ")
|
||||
.append(" " + apkPath + " ")
|
||||
.append(" -digestalg SHA1 -sigalg SHA1withRSA ")
|
||||
.append(" key0 ");
|
||||
// System.out.println("\n" + signCmd + "\n");
|
||||
String result = ShellCmdUtil.execCmd(signCmd.toString(), null);
|
||||
System.out.println(" sign apk time is :" + ((System.currentTimeMillis() - time) / 1000) +
|
||||
"s\n\n" + " result=" + result);
|
||||
return true;
|
||||
}
|
||||
System.out.println(" keystore not exist :" + keystoreFile.getAbsolutePath() +
|
||||
" please sign the apk by hand. \n");
|
||||
return false;
|
||||
} catch (Throwable e) {
|
||||
System.out.println("use default jarsigner to sign apk failed, fail msg is :" +
|
||||
e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAndroid() {
|
||||
boolean isAndroid = true;
|
||||
try {
|
||||
Class.forName("android.content.Context");
|
||||
} catch (ClassNotFoundException e) {
|
||||
isAndroid = false;
|
||||
}
|
||||
return isAndroid;
|
||||
}
|
||||
|
||||
// 使用Android build-tools里自带的apksigner工具进行签名
|
||||
private boolean signApkUsingAndroidApksigner(String apkPath, String keyStorePath, String signedApkPath, String keyStorePassword) {
|
||||
ArrayList<String> commandList = new ArrayList<>();
|
||||
|
||||
commandList.add("sign");
|
||||
commandList.add("--ks");
|
||||
commandList.add(keyStorePath);
|
||||
commandList.add("--ks-key-alias");
|
||||
commandList.add("key0");
|
||||
commandList.add("--ks-pass");
|
||||
commandList.add("pass:" + keyStorePassword);
|
||||
commandList.add("--key-pass");
|
||||
commandList.add("pass:" + keyStorePassword);
|
||||
commandList.add("--out");
|
||||
commandList.add(signedApkPath);
|
||||
commandList.add("--v1-signing-enabled");
|
||||
commandList.add("true");
|
||||
commandList.add("--v2-signing-enabled"); // v2签名不兼容android 6
|
||||
commandList.add("false");
|
||||
commandList.add("--v3-signing-enabled"); // v3签名不兼容android 6
|
||||
commandList.add("false");
|
||||
commandList.add(apkPath);
|
||||
|
||||
int size = commandList.size();
|
||||
String[] commandArray = new String[size];
|
||||
commandArray = commandList.toArray(commandArray);
|
||||
|
||||
try {
|
||||
ApkSignerTool.main(commandArray);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.storm.wind.xpatch.task;
|
||||
|
||||
import com.storm.wind.xpatch.util.ApkSignatureHelper;
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class SaveApkSignatureTask implements Runnable {
|
||||
|
||||
private String apkPath;
|
||||
private String dstFilePath;
|
||||
|
||||
private final static String SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini";
|
||||
|
||||
public SaveApkSignatureTask(String apkPath, String unzipApkFilePath) {
|
||||
this.apkPath = apkPath;
|
||||
this.dstFilePath = (unzipApkFilePath + SIGNATURE_INFO_ASSET_PATH).replace("/", File.separator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// First, get the original signature
|
||||
String originalSignature = ApkSignatureHelper.getApkSignInfo(apkPath);
|
||||
if (originalSignature == null || originalSignature.isEmpty()) {
|
||||
System.out.println(" Get original signature failed !!!!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Then, save the signature chars to the asset file
|
||||
File file = new File(dstFilePath);
|
||||
File fileParent = file.getParentFile();
|
||||
if (!fileParent.exists()) {
|
||||
if(!fileParent.mkdirs()){
|
||||
System.out.println("mkdir fails " + fileParent.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
FileUtils.writeFile(dstFilePath, originalSignature);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.storm.wind.xpatch.task;
|
||||
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Created by xiawanli on 2019/4/6
|
||||
*/
|
||||
public class SaveOriginalApplicationNameTask implements Runnable {
|
||||
|
||||
private final String applcationName;
|
||||
private final String unzipApkFilePath;
|
||||
private String dstFilePath;
|
||||
|
||||
private final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini";
|
||||
|
||||
public SaveOriginalApplicationNameTask(String applicationName, String unzipApkFilePath) {
|
||||
this.applcationName = applicationName;
|
||||
this.unzipApkFilePath = unzipApkFilePath;
|
||||
|
||||
this.dstFilePath = (unzipApkFilePath + APPLICATION_NAME_ASSET_PATH).replace("/", File.separator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ensureDstFileCreated();
|
||||
FileUtils.writeFile(dstFilePath, applcationName);
|
||||
}
|
||||
|
||||
private void ensureDstFileCreated() {
|
||||
File dstParentFile = new File(dstFilePath);
|
||||
if (!dstParentFile.getParentFile().getParentFile().exists()) {
|
||||
if(!dstParentFile.getParentFile().getParentFile().mkdirs()){
|
||||
throw new IllegalStateException("mkdir fail");
|
||||
}
|
||||
}
|
||||
if (!dstParentFile.getParentFile().exists()) {
|
||||
if(!dstParentFile.getParentFile().mkdirs()){
|
||||
throw new IllegalStateException("mkdir fail");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.storm.wind.xpatch.task;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class SoAndDexCopyTask implements Runnable {
|
||||
|
||||
private final String[] APK_LIB_PATH_ARRAY = {
|
||||
"lib/armeabi-v7a/",
|
||||
"lib/armeabi/",
|
||||
"lib/arm64-v8a/",
|
||||
"lib/x86",
|
||||
"lib/x86_64"
|
||||
};
|
||||
|
||||
private int dexFileCount;
|
||||
private String unzipApkFilePath;
|
||||
|
||||
public SoAndDexCopyTask(int dexFileCount, String unzipApkFilePath) {
|
||||
this.dexFileCount = dexFileCount;
|
||||
this.unzipApkFilePath = unzipApkFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 复制xposed兼容层的dex文件以及so文件到当前目录下
|
||||
copySoFile();
|
||||
copyDexFile(dexFileCount);
|
||||
|
||||
// 删除签名信息
|
||||
deleteMetaInfo();
|
||||
}
|
||||
|
||||
private void copySoFile() {
|
||||
List<String> existLibPathArray = new ArrayList<>();
|
||||
for (String libPath : APK_LIB_PATH_ARRAY) {
|
||||
String apkSoFullPath = fullLibPath(libPath);
|
||||
File apkSoFullPathFile = new File(apkSoFullPath);
|
||||
if (apkSoFullPathFile.exists()) {
|
||||
existLibPathArray.add(libPath);
|
||||
} else {
|
||||
System.out.println("target app dont have " + libPath + ", skip");
|
||||
}
|
||||
}
|
||||
|
||||
if (existLibPathArray.isEmpty()) {
|
||||
System.out.println("target app dont have any so in \"lib/{eabi}\" dir, so create default \"armeabi-v7a\"");
|
||||
String libPath = APK_LIB_PATH_ARRAY[0];
|
||||
String apkSoFullPath = fullLibPath(libPath);
|
||||
File apkSoFullPathFile = new File(apkSoFullPath);
|
||||
if (apkSoFullPathFile.mkdirs()) {
|
||||
throw new IllegalStateException("mkdir fail " + apkSoFullPathFile.getAbsolutePath());
|
||||
}
|
||||
existLibPathArray.add(libPath);
|
||||
}
|
||||
|
||||
for (String libPath : existLibPathArray) {
|
||||
if (libPath == null || libPath.isEmpty()) {
|
||||
throw new IllegalStateException("fail eabi path");
|
||||
}
|
||||
|
||||
String apkSoFullPath = fullLibPath(libPath);
|
||||
String eabi = libPath.substring(libPath.indexOf("/"));
|
||||
if (eabi.isEmpty()) {
|
||||
throw new IllegalStateException("fail find eabi in " + libPath);
|
||||
}
|
||||
|
||||
File[] files = new File("list-so", eabi).listFiles();
|
||||
if (files == null) {
|
||||
System.out.println("Warning: Nothing so file has been copied in " + libPath);
|
||||
continue;
|
||||
}
|
||||
for (File mySoFile : files) {
|
||||
File target = new File(apkSoFullPath, mySoFile.getName());
|
||||
try {
|
||||
FileUtils.copyFile(mySoFile, target);
|
||||
} catch (Exception err) {
|
||||
throw new IllegalStateException("wtf", err);
|
||||
}
|
||||
System.out.println("Copy " + mySoFile.getAbsolutePath() + " to " + target.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDexFile(int dexFileCount) {
|
||||
try {
|
||||
// copy all dex files in list-dex
|
||||
File[] files = new File("list-dex").listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
System.out.println("Warning: Nothing dex file has been copied");
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
|
||||
File target = new File(unzipApkFilePath, copiedDexFileName);
|
||||
FileUtils.copyFile(file, target);
|
||||
System.out.println("Copy " + file.getAbsolutePath() + " to " + target.getAbsolutePath());
|
||||
dexFileCount++;
|
||||
}
|
||||
} catch (Exception err) {
|
||||
throw new IllegalStateException("wtf", err);
|
||||
}
|
||||
}
|
||||
|
||||
private String fullLibPath(String libPath) {
|
||||
return unzipApkFilePath + libPath.replace("/", File.separator);
|
||||
}
|
||||
|
||||
private void deleteMetaInfo() {
|
||||
String metaInfoFilePath = "META-INF";
|
||||
File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);
|
||||
if (!metaInfoFileRoot.exists()) {
|
||||
return;
|
||||
}
|
||||
File[] childFileList = metaInfoFileRoot.listFiles();
|
||||
if (childFileList == null || childFileList.length == 0) {
|
||||
return;
|
||||
}
|
||||
for (File file : childFileList) {
|
||||
String fileName = file.getName().toUpperCase();
|
||||
if (fileName.endsWith(".MF") || fileName.endsWith(".RAS") || fileName.endsWith(".SF")) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.storm.wind.xpatch.util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class ApkSignatureHelper {
|
||||
|
||||
private static char[] toChars(byte[] mSignature) {
|
||||
byte[] sig = mSignature;
|
||||
final int N = sig.length;
|
||||
final int N2 = N * 2;
|
||||
char[] text = new char[N2];
|
||||
for (int j = 0; j < N; j++) {
|
||||
byte v = sig[j];
|
||||
int d = (v >> 4) & 0xf;
|
||||
text[j * 2] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d));
|
||||
d = v & 0xf;
|
||||
text[j * 2 + 1] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) {
|
||||
try {
|
||||
InputStream is = jarFile.getInputStream(je);
|
||||
while (is.read(readBuffer, 0, readBuffer.length) != -1) {
|
||||
}
|
||||
is.close();
|
||||
return (Certificate[]) (je != null ? je.getCertificates() : null);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getApkSignInfo(String apkFilePath) {
|
||||
byte[] readBuffer = new byte[8192];
|
||||
Certificate[] certs = null;
|
||||
try {
|
||||
JarFile jarFile = new JarFile(apkFilePath);
|
||||
Enumeration<?> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry je = (JarEntry) entries.nextElement();
|
||||
if (je.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (je.getName().startsWith("META-INF/")) {
|
||||
continue;
|
||||
}
|
||||
Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
|
||||
if (certs == null) {
|
||||
certs = localCerts;
|
||||
} else {
|
||||
for (int i = 0; i < certs.length; i++) {
|
||||
boolean found = false;
|
||||
for (int j = 0; j < localCerts.length; j++) {
|
||||
if (certs[i] != null && certs[i].equals(localCerts[j])) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found || certs.length != localCerts.length) {
|
||||
jarFile.close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jarFile.close();
|
||||
System.out.println("getApkSignInfo result: " + certs[0]);
|
||||
return new String(toChars(certs[0].getEncoded()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
package com.storm.wind.xpatch.util;
|
||||
|
||||
import org.apache.commons.io.FileSystemUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.CheckedOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
static final int BUFFER = 8192;
|
||||
|
||||
/**
|
||||
* 解压文件
|
||||
*
|
||||
* @param zipPath 要解压的目标文件
|
||||
* @param descDir 指定解压目录
|
||||
* @return 解压结果:成功,失败
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void decompressZip(String zipPath, String descDir) throws IOException {
|
||||
File zipFile = new File(zipPath);
|
||||
if (!descDir.endsWith(File.separator)) {
|
||||
descDir = descDir + File.separator;
|
||||
}
|
||||
File pathFile = new File(descDir);
|
||||
if (!pathFile.exists()) {
|
||||
if (!pathFile.mkdirs()) {
|
||||
throw new IllegalStateException("mkdir fail " + pathFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
try (ZipFile zip = new ZipFile(zipFile, Charset.forName("gbk"))) {
|
||||
for (Enumeration entries = zip.entries(); entries.hasMoreElements(); ) {
|
||||
ZipEntry entry = (ZipEntry) entries.nextElement();
|
||||
String zipEntryName = entry.getName();
|
||||
|
||||
String outPath = (descDir + zipEntryName).replace("/", File.separator);
|
||||
File file = new File(outPath);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!file.mkdirs()) {
|
||||
throw new IllegalStateException("mkdir fail " + file.getAbsolutePath());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try (InputStream in = zip.getInputStream(entry)) {
|
||||
if (file.getParentFile() != null && !file.getParentFile().exists()) {
|
||||
if (!file.getParentFile().mkdirs()) {
|
||||
throw new IllegalStateException("mkdir fail " + file.getAbsolutePath());
|
||||
}
|
||||
if (System.getProperty("os.name", "").toLowerCase().contains("win")) {
|
||||
Runtime.getRuntime().exec("fsutil file setCaseSensitiveInfo " + file.getParentFile().getAbsolutePath());
|
||||
System.out.println("Enable setCaseSensitiveInfo for " + file.getParentFile().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
OutputStream out = new FileOutputStream(outPath);
|
||||
IOUtils.copy(in, out);
|
||||
out.close();
|
||||
} catch (Exception err) {
|
||||
throw new IllegalStateException("wtf", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream getInputStreamFromFile(String filePath) {
|
||||
return FileUtils.class.getClassLoader().getResourceAsStream(filePath);
|
||||
}
|
||||
|
||||
// copy an asset file into a path
|
||||
public static void copyFileFromJar(String inJarPath, String distPath) {
|
||||
|
||||
// System.out.println("start copyFile inJarPath =" + inJarPath + " distPath = " + distPath);
|
||||
InputStream inputStream = getInputStreamFromFile(inJarPath);
|
||||
|
||||
BufferedInputStream in = null;
|
||||
BufferedOutputStream out = null;
|
||||
try {
|
||||
in = new BufferedInputStream(inputStream);
|
||||
out = new BufferedOutputStream(new FileOutputStream(distPath));
|
||||
|
||||
int len = -1;
|
||||
byte[] b = new byte[1024];
|
||||
while ((len = in.read(b)) != -1) {
|
||||
out.write(b, 0, len);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
close(out);
|
||||
close(in);
|
||||
}
|
||||
}
|
||||
|
||||
public static void compressToZip(String srcPath, String dstPath) {
|
||||
File srcFile = new File(srcPath);
|
||||
File dstFile = new File(dstPath);
|
||||
if (!srcFile.exists()) {
|
||||
System.out.println(srcPath + " does not exist !");
|
||||
return;
|
||||
}
|
||||
|
||||
FileOutputStream out = null;
|
||||
ZipOutputStream zipOut = null;
|
||||
try {
|
||||
out = new FileOutputStream(dstFile);
|
||||
CheckedOutputStream cos = new CheckedOutputStream(out, new CRC32());
|
||||
zipOut = new ZipOutputStream(cos);
|
||||
String baseDir = "";
|
||||
compress(srcFile, zipOut, baseDir, true);
|
||||
} catch (IOException e) {
|
||||
System.out.println(" compress exception = " + e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (zipOut != null) {
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
close(zipOut);
|
||||
close(out);
|
||||
}
|
||||
}
|
||||
|
||||
private static void compress(File file, ZipOutputStream zipOut, String baseDir, boolean isRootDir) throws IOException {
|
||||
if (file.isDirectory()) {
|
||||
compressDirectory(file, zipOut, baseDir, isRootDir);
|
||||
} else {
|
||||
compressFile(file, zipOut, baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩一个目录
|
||||
*/
|
||||
private static void compressDirectory(File dir, ZipOutputStream zipOut, String baseDir, boolean isRootDir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
String compressBaseDir = "";
|
||||
if (!isRootDir) {
|
||||
compressBaseDir = baseDir + dir.getName() + "/";
|
||||
}
|
||||
compress(files[i], zipOut, compressBaseDir, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩一个文件
|
||||
*/
|
||||
private static void compressFile(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedInputStream bis = null;
|
||||
try {
|
||||
bis = new BufferedInputStream(new FileInputStream(file));
|
||||
ZipEntry entry = new ZipEntry(baseDir + file.getName());
|
||||
zipOut.putNextEntry(entry);
|
||||
int count;
|
||||
byte data[] = new byte[BUFFER];
|
||||
while ((count = bis.read(data, 0, BUFFER)) != -1) {
|
||||
zipOut.write(data, 0, count);
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (null != bis) {
|
||||
bis.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFile(String filePath, String content) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (content == null || content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File dstFile = new File(filePath);
|
||||
|
||||
if (!dstFile.getParentFile().exists()) {
|
||||
dstFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
FileOutputStream outputStream = null;
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dstFile);
|
||||
writer = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
writer.write(content);
|
||||
writer.flush();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
close(outputStream);
|
||||
close(writer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void close(Closeable closeable) {
|
||||
try {
|
||||
if (closeable != null) {
|
||||
closeable.close();
|
||||
}
|
||||
} catch (IOException io) {
|
||||
io.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.storm.wind.xpatch.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import wind.android.content.res.AXmlResourceParser;
|
||||
import wind.v1.XmlPullParser;
|
||||
import wind.v1.XmlPullParserException;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class ManifestParser {
|
||||
|
||||
/**
|
||||
* Get the package name and the main application name from the manifest file
|
||||
* */
|
||||
public static Pair parseManifestFile(String filePath) {
|
||||
AXmlResourceParser parser = new AXmlResourceParser();
|
||||
File file = new File(filePath);
|
||||
String packageName = null;
|
||||
String applicationName = null;
|
||||
if (!file.exists()) {
|
||||
System.out.println(" manifest file not exist!!! filePath -> " + filePath);
|
||||
return null;
|
||||
}
|
||||
FileInputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(file);
|
||||
|
||||
parser.open(inputStream);
|
||||
|
||||
while (true) {
|
||||
int type = parser.next();
|
||||
if (type == XmlPullParser.END_DOCUMENT) {
|
||||
break;
|
||||
}
|
||||
if (type == XmlPullParser.START_TAG) {
|
||||
int attrCount = parser.getAttributeCount();
|
||||
for (int i = 0; i < attrCount; i++) {
|
||||
String attrName = parser.getAttributeName(i);
|
||||
|
||||
String name = parser.getName();
|
||||
|
||||
if ("manifest".equals(name)) {
|
||||
if ("package".equals(attrName)) {
|
||||
packageName = parser.getAttributeValue(i);
|
||||
}
|
||||
}
|
||||
|
||||
if ("application".equals(name)) {
|
||||
if ("name".equals(attrName)) {
|
||||
applicationName = parser.getAttributeValue(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (packageName != null && packageName.length() > 0 && applicationName != null && applicationName.length() > 0) {
|
||||
return new Pair(packageName, applicationName);
|
||||
}
|
||||
}
|
||||
} else if (type == XmlPullParser.END_TAG) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("parseManifestFile failed, reason --> " + e.getMessage());
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Pair(packageName, applicationName);
|
||||
}
|
||||
|
||||
public static class Pair {
|
||||
public String packageName;
|
||||
public String applicationName;
|
||||
|
||||
public Pair(String packageName, String applicationName) {
|
||||
this.packageName = packageName;
|
||||
this.applicationName = applicationName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
package com.storm.wind.xpatch.util;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ReflectUtils {
|
||||
|
||||
//获取类的实例的变量的值
|
||||
public static Object getField(Object receiver, String fieldName) {
|
||||
return getField(null, receiver, fieldName);
|
||||
}
|
||||
|
||||
//获取类的静态变量的值
|
||||
public static Object getField(String className, String fieldName) {
|
||||
return getField(className, null, fieldName);
|
||||
}
|
||||
|
||||
public static Object getField(Class<?> clazz, String className, String fieldName, Object receiver) {
|
||||
try {
|
||||
if (clazz == null) {
|
||||
clazz = Class.forName(className);
|
||||
}
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return field.get(receiver);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object getField(String className, Object receiver, String fieldName) {
|
||||
Class<?> clazz = null;
|
||||
Field field;
|
||||
if (className != null && className.length() > 0) {
|
||||
try {
|
||||
clazz = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
if (receiver != null) {
|
||||
clazz = receiver.getClass();
|
||||
}
|
||||
}
|
||||
if (clazz == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
field = findField(clazz, fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return field.get(receiver);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object setField(Object receiver, String fieldName, Object value) {
|
||||
try {
|
||||
Field field;
|
||||
field = findField(receiver.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
Object old = field.get(receiver);
|
||||
field.set(receiver, value);
|
||||
return old;
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object setField(Class<?> clazz, Object receiver, String fieldName, Object value) {
|
||||
try {
|
||||
Field field;
|
||||
field = findField(clazz, fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
Object old = field.get(receiver);
|
||||
field.set(receiver, value);
|
||||
return old;
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object callMethod(Object receiver, String methodName, Object... params) {
|
||||
return callMethod(null, receiver, methodName, params);
|
||||
}
|
||||
|
||||
public static Object setField(String clazzName, Object receiver, String fieldName, Object value) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(clazzName);
|
||||
Field field;
|
||||
field = findField(clazz, fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
Object old = field.get(receiver);
|
||||
field.set(receiver, value);
|
||||
return old;
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static Object callMethod(String className, String methodName, Object... params) {
|
||||
return callMethod(className, null, methodName, params);
|
||||
}
|
||||
|
||||
public static Object callMethod(Class<?> clazz, String className, String methodName, Object receiver,
|
||||
Class[] types, Object... params) {
|
||||
try {
|
||||
if (clazz == null) {
|
||||
clazz = Class.forName(className);
|
||||
}
|
||||
Method method = clazz.getDeclaredMethod(methodName, types);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(receiver, params);
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object callMethod(String className, Object receiver, String methodName, Object... params) {
|
||||
Class<?> clazz = null;
|
||||
if (className != null && className.length() > 0) {
|
||||
try {
|
||||
clazz = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
if (receiver != null) {
|
||||
clazz = receiver.getClass();
|
||||
}
|
||||
}
|
||||
if (clazz == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Method method = findMethod(clazz, methodName, params);
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
return method.invoke(receiver, params);
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> clazz, String name, Object... arg) {
|
||||
Method[] methods = clazz.getMethods();
|
||||
Method method = null;
|
||||
for (Method m : methods) {
|
||||
if (methodFitParam(m, name, arg)) {
|
||||
method = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (method == null) {
|
||||
method = findDeclaredMethod(clazz, name, arg);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private static Method findDeclaredMethod(Class<?> clazz, String name, Object... arg) {
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
Method method = null;
|
||||
for (Method m : methods) {
|
||||
if (methodFitParam(m, name, arg)) {
|
||||
method = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (method == null) {
|
||||
if (clazz.equals(Object.class)) {
|
||||
return null;
|
||||
}
|
||||
return findDeclaredMethod(clazz.getSuperclass(), name, arg);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private static boolean methodFitParam(Method method, String methodName, Object... arg) {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
if (arg == null || arg.length == 0) {
|
||||
return paramTypes == null || paramTypes.length == 0;
|
||||
}
|
||||
if (paramTypes.length != arg.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < arg.length; ++i) {
|
||||
Object ar = arg[i];
|
||||
Class<?> paramT = paramTypes[i];
|
||||
if (ar == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//TODO for primitive type
|
||||
if (paramT.isPrimitive()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!paramT.isInstance(ar)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Field findField(Class<?> clazz, String name) {
|
||||
try {
|
||||
return clazz.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
if (clazz.equals(Object.class)) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
Class<?> base = clazz.getSuperclass();
|
||||
return findField(base, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.storm.wind.xpatch.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public class ShellCmdUtil {
|
||||
|
||||
/**
|
||||
* 执行系统命令, 返回执行结果
|
||||
*
|
||||
* @param cmd 需要执行的命令
|
||||
* @param dir 执行命令的子进程的工作目录, null 表示和当前主进程工作目录相同
|
||||
*/
|
||||
public static String execCmd(String cmd, File dir) throws Exception {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
Process process = null;
|
||||
BufferedReader bufrIn = null;
|
||||
BufferedReader bufrError = null;
|
||||
|
||||
try {
|
||||
// 执行命令, 返回一个子进程对象(命令在子进程中执行)
|
||||
process = Runtime.getRuntime().exec(cmd, null, dir);
|
||||
|
||||
// 方法阻塞, 等待命令执行完成(成功会返回0)
|
||||
process.waitFor();
|
||||
|
||||
// 获取命令执行结果, 有两个结果: 正常的输出 和 错误的输出(PS: 子进程的输出就是主进程的输入)
|
||||
bufrIn = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
|
||||
bufrError = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
|
||||
|
||||
// 读取输出
|
||||
String line = null;
|
||||
while ((line = bufrIn.readLine()) != null) {
|
||||
result.append(line).append('\n');
|
||||
}
|
||||
while ((line = bufrError.readLine()) != null) {
|
||||
result.append(line).append('\n');
|
||||
}
|
||||
|
||||
} finally {
|
||||
close(bufrIn);
|
||||
close(bufrError);
|
||||
|
||||
// 销毁子进程
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 返回执行结果
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static void chmodNoException(String path, int mode) {
|
||||
try {
|
||||
chmod(path, mode);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("chmod exception path --> " + path + " exception -->" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void chmod(String path, int mode) throws Exception {
|
||||
chmodOnAndroid(path, mode);
|
||||
|
||||
File file = new File(path);
|
||||
String cmd = "chmod ";
|
||||
if (file.isDirectory()) {
|
||||
cmd += " -R ";
|
||||
}
|
||||
String cmode = String.format("%o", mode);
|
||||
Runtime.getRuntime().exec(cmd + cmode + " " + path).waitFor();
|
||||
}
|
||||
|
||||
private static void chmodOnAndroid(String path, int mode) {
|
||||
Object sdk_int = ReflectUtils.getField("android.os.Build$VERSION", "SDK_INT");
|
||||
if (!(sdk_int instanceof Integer)) {
|
||||
return;
|
||||
}
|
||||
if ((int)sdk_int >= 21) {
|
||||
System.out.println("chmod on android is called, path = " + path);
|
||||
ReflectUtils.callMethod("android.system.Os", "chmod", path, mode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void close(Closeable stream) {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (Exception e) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface FileMode {
|
||||
int MODE_ISUID = 04000;
|
||||
int MODE_ISGID = 02000;
|
||||
int MODE_ISVTX = 01000;
|
||||
int MODE_IRUSR = 00400;
|
||||
int MODE_IWUSR = 00200;
|
||||
int MODE_IXUSR = 00100;
|
||||
int MODE_IRGRP = 00040;
|
||||
int MODE_IWGRP = 00020;
|
||||
int MODE_IXGRP = 00010;
|
||||
int MODE_IROTH = 00004;
|
||||
int MODE_IWOTH = 00002;
|
||||
int MODE_IXOTH = 00001;
|
||||
|
||||
int MODE_755 = MODE_IRUSR | MODE_IWUSR | MODE_IXUSR
|
||||
| MODE_IRGRP | MODE_IXGRP
|
||||
| MODE_IROTH | MODE_IXOTH;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package org.lsposed.patch;
|
||||
|
||||
public class LSPatch {
|
||||
}
|
||||
|
|
@ -12,3 +12,4 @@ include ':hiddenapi-bridge'
|
|||
project(':hiddenapi-bridge').projectDir = new File('mmp/hiddenapi-bridge')
|
||||
include ':manager-service'
|
||||
project(':manager-service').projectDir = new File('mmp/manager-service')
|
||||
include ':patch'
|
||||
|
|
|
|||
Loading…
Reference in New Issue