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')
|
project(':hiddenapi-bridge').projectDir = new File('mmp/hiddenapi-bridge')
|
||||||
include ':manager-service'
|
include ':manager-service'
|
||||||
project(':manager-service').projectDir = new File('mmp/manager-service')
|
project(':manager-service').projectDir = new File('mmp/manager-service')
|
||||||
|
include ':patch'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue