remove old
This commit is contained in:
parent
c7eff6fe1f
commit
f4343d8e17
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
apply plugin: 'java-library'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
compile project(':axmlprinter')
|
|
||||||
compile project(':apksigner')
|
|
||||||
compile group: 'commons-io', name: 'commons-io', version: '2.8.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
|
||||||
baseName = "mmpatch"
|
|
||||||
manifest {
|
|
||||||
attributes 'Main-Class': 'com.storm.wind.xpatch.MainCommand'
|
|
||||||
}
|
|
||||||
//添加将引用的jar的源码打入最终的jar
|
|
||||||
from {
|
|
||||||
(configurations.runtime).collect {
|
|
||||||
it.isDirectory() ? it : zipTree(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
from fileTree(dir: 'src/main', includes: ['assets/**'])
|
|
||||||
|
|
||||||
//排除引用的jar中的签名信息
|
|
||||||
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.MF'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.build.doLast {
|
|
||||||
println("Build to " + jar.archivePath)
|
|
||||||
println("Try \'java -jar " + jar.archiveName + "\' find more help")
|
|
||||||
}
|
|
||||||
|
|
||||||
//添加源码中引入的非代码文件,例如资源等
|
|
||||||
sourceSets.main.resources {
|
|
||||||
srcDirs = [
|
|
||||||
"src/main/java",
|
|
||||||
];
|
|
||||||
include "**/*.*"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,287 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,478 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,267 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue