package javafx.apktools.bin; import javafx.apktools.model.manifest.Manifest; import javafx.apktools.model.manifest.MetaData; import javafx.apktools.model.resource.Bools; import javafx.apktools.model.resource.Resource; import javafx.apktools.model.resource.Strings; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; import java.io.*; import java.nio.channels.FileChannel; import java.util.List; /** * 定制APK命令类 */ public class Command { /** * 回调接口 */ private Callback callback; public Command(Callback callback) { this.callback = callback; } /** * 解包apk文件,需要aoktool.jar文件 * * @param apkFilePath apk文件路径 * @param outPath 解压路径 */ public boolean decodeApk(String apkFilePath, String outPath) { // return executeCommand("java", "-jar", "apktool.jar", "d", "-f", apkFilePath, "-o", outPath); try { brut.apktool.Main.main(new String[]{"d", "-f", apkFilePath, "-o", outPath}); return true; } catch (Exception e) { e.printStackTrace(); callback("解包失败 !!!!!\r\n" + e.getMessage()); } return false; } /** * 打包apk文件,需要aoktool.jar文件 * * @param buildApkFolderPath 解包apk后的文件夹路径 * @param buildApkOutPath 打包后的文件存放路径 */ public boolean buildApk(String buildApkFolderPath, String buildApkOutPath) { // return executeCommand("java", "-jar", "apktool.jar", "b", buildApkFolderPath, "-o", buildApkOutPath); try { brut.apktool.Main.main(new String[]{"b", buildApkFolderPath, "-o", buildApkOutPath}); return true; } catch (Exception e) { e.printStackTrace(); callback("打包失败 !!!!!\r\n" + e.getMessage()); } return false; } /** * 需要安装JDK * 带时间戳的apk签名,这个需要联网。 * * @param keystoreFilePath 签名文件路径 * @param apkFilePath 被签名apk文件路径 * @param alias 签名文件的alias名称 * @param password 签名文件密码 */ public boolean signerApkByTime(String keystoreFilePath, String apkFilePath, String alias, String password) { return executeCommand("jarsigner", "-verbose", "-sigalg", "SHA1withRSA", "-tsa", "https://timestamp.geotrust.com/tsa", "-digestalg", "SHA1", "-keystore", keystoreFilePath, apkFilePath, alias, "-storepass", password); } /** * 需要安装JDK * 不带时间错的apk签名,这个不需要联网。 * * @param keystoreFilePath 签名文件路径 * @param apkFilePath 被签名apk文件路径 * @param alias 签名文件的alias名称 * @param password 签名文件密码 */ public boolean signerApk(String keystoreFilePath, String apkFilePath, String alias, String password) { return executeCommand("jarsigner", "-verbose", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", keystoreFilePath, apkFilePath, alias, "-storepass", password); } /** * 需要安装并Android SDK并配置环境变量Build Tools路径 * 优化apk文件,这个需要Android Build Tools 中的zipalign程序文件 * * @param apkFilePath 要优化的apk文件路径 * @param outFilePath 优化后的apk存放文件路径 */ public boolean zipalign(String apkFilePath, String outFilePath) { return executeCommand("zipalign", "-f", "-v", "4", apkFilePath, outFilePath); } /** * 替换资源文件 * * @param replaceResourceFolder 替换res资源的路径 * @param appFolderName 被替换res资源的路径 */ public void replaceResource(String replaceResourceFolder, String appFolderName) { File infile = new File(replaceResourceFolder); File outFile = new File(appFolderName); copyFile(infile, outFile); } /** * 递归文件夹及文件 * * @param inputFile 替换文件文件路径 * @param outputFile 被替换文件文件路径 */ private void copyFile(File inputFile, File outputFile) { if (!inputFile.exists()) { return; } if (inputFile.isDirectory()) { File[] listFile = inputFile.listFiles((pathname) -> !pathname.getName().startsWith(".")); for (File inFile : listFile) { copyFile(inFile, outputFile); } } else { File replaceFile = new File(outputFile + File.separator + "res" + File.separator + inputFile.getParentFile().getName() + File.separator + inputFile.getName()); File replaceFileV4 = new File(outputFile + File.separator + "res" + File.separator + inputFile.getParentFile().getName() + "-v4" + File.separator + inputFile.getName()); if (replaceFile.exists()) { copyFileStream(inputFile, replaceFile); callback(String.format("拷贝文件 [%s] 至 ——> [%s]", inputFile.getPath(), replaceFile.getPath())); } if (replaceFileV4.exists()) { copyFileStream(inputFile, replaceFileV4); callback(String.format("拷贝文件 [%s] 至 ——> [%s]", inputFile.getPath(), replaceFileV4.getPath())); } } } /** * 修改AndroidManifest.xml文件 * * @param appFolderName AndroidManifest.xml文件所在路径 * @param manifest 要修改的信息 */ public boolean updateAndroidManifest(String appFolderName, Manifest manifest) { if (manifest == null) { return false; } try { File androidManifestFile = new File(appFolderName + File.separator + "AndroidManifest.xml"); Document document = new SAXReader().read(androidManifestFile); Element element = document.getRootElement().element("application"); List<Element> list = element.elements("meta-data"); List<MetaData> metaData = manifest.getMetaData(); boolean isUpdate = false; for (MetaData data : metaData) { String name = data.getName(); String value = data.getValue(); for (Element s : list) { Attribute attribute = s.attribute("name"); if (attribute.getValue().equals(name)) { s.attribute("value").setValue(value); isUpdate = true; callback("更新 AndroidManifest.xml meta-data name='" + attribute.getValue() + "' value='" + value + "'"); } } } // OutputFormat format = OutputFormat.createCompactFormat(); // format.setNewlines(true); // format.setTrimText(true); // format.setIndent(true); // format.setIndentSize(4); // XMLWriter writer = new XMLWriter(new FileOutputStream(androidManifestFile), format); if(isUpdate){ XMLWriter writer = new XMLWriter(new FileOutputStream(androidManifestFile)); writer.write(document); writer.close(); callback("更新 AndroidManifest.xml 完成 ~ "); } } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 修改apktool.jar解包后生成的apktool.yml文件 * * @param appFolderName apktool.yml文件所在路径 * @param versionName 要改的版本名称 */ public boolean updateApkToolYmlVersion(String appFolderName, String versionName) { try { File apkToolYmlFile = new File(appFolderName + File.separator + "apktool.yml"); BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(apkToolYmlFile))); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { if (line.contains("versionCode")) { line = " versionCode: '" + getVersionCode(versionName) + "'"; callback("更新 apktool.yml " + line); } else if (line.contains("versionName")) { line = " versionName: '" + versionName + "'"; callback("更新 apktool.yml " + line); } sb.append(line).append("\r\n"); } close(reader); FileOutputStream out = new FileOutputStream(apkToolYmlFile); out.write(sb.toString().getBytes()); close(out); callback("更新 apktool.yml 完成 ~ "); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 更改string.xml文件内容 * * @param appFolderName 目标文件夹 * @param resource 替换的resource资源 */ public boolean updateResource(String appFolderName, Resource resource) { String valuesFolderPath = appFolderName + File.separator + "res" + File.separator + "values" + File.separator; updateStrings(new File(valuesFolderPath + "strings.xml"), resource.getStrings()); updateBools(new File(valuesFolderPath + "bools.xml"), resource.getBools()); return true; } /** * 修改strings.xml文件内容 * * @param file strings文件 * @param strings 修改的值列表 */ private void updateStrings(File file, List<Strings> strings) { try { if (strings == null || strings.isEmpty()) { return; } Document document = new SAXReader().read(file); List<Element> elements = document.getRootElement().elements(); elements.forEach(element -> { final String name = element.attribute("name").getValue(); strings.forEach(s -> { if (s.getName().equals(name)) { element.setText(s.getValue()); callback("修改 strings.xml name='" + name + "' value='" + s.getValue() + "'"); } }); }); XMLWriter writer = new XMLWriter(new FileOutputStream(file)); writer.write(document); writer.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 修改bools.xml文件内容 * * @param file bools文件 * @param bools 修改的值列表 */ private void updateBools(File file, List<Bools> bools) { try { if (bools == null || bools.isEmpty()) { return; } Document document = new SAXReader().read(file); List<Element> elements = document.getRootElement().elements(); elements.forEach(element -> { final String name = element.attribute("name").getValue(); bools.forEach(s -> { if (s.getName().equals(name)) { element.setText(s.getValue()); callback("修改 bools.xml name='" + name + "' value='" + s.getValue() + "'"); } }); }); XMLWriter writer = new XMLWriter(new FileOutputStream(file)); writer.write(document); writer.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 执行命令 * * @param command 命令 */ private synchronized boolean executeCommand(String... command) { Process process = null; BufferedReader reader = null; try { ProcessBuilder builder = new ProcessBuilder(); builder.command(command); builder.redirectErrorStream(true); process = builder.start(); reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { callback(line); if (line.contains("Exception") || line.contains("Unable to open")) { return false; } } return true; } catch (IOException e) { e.printStackTrace(); callback(e.getMessage()); } finally { close(reader); if (process != null) { process.destroy(); } } return false; } /** * 按预定返回版本号 * * @param versionName 版本名称 * @return 版本号 */ public String getVersionCode(String versionName) { char[] chars = versionName.toCharArray(); for (char c : chars) { if (c == '.') { continue; } if (!Character.isDigit(c)) { throw new NumberFormatException("版本填写错误,不能包涵非数字和.点的字符"); } } int version = 100000000; String[] strings = versionName.split("\\."); if (strings.length != 3) { throw new NumberFormatException("版本填写错误,请使用x.x.x的格式"); } int major = Integer.parseInt(strings[0]) * 10000; int minor = Integer.parseInt(strings[1]) * 100; int build = Integer.parseInt(strings[2]); return String.valueOf(version + major + minor + build); } /** * 递归删除文件 * * @param file 文件或文件夹 */ public void deleteFile(File file) { if (file.isFile()) { file.delete(); } else { File[] files = file.listFiles(); for (File f : files) { deleteFile(f); } file.delete(); } } /** * NIO文件拷贝 * * @param inFile 被拷贝的文件 * @param outFile 拷贝的文件 */ public boolean copyFileStream(File inFile, File outFile) { FileInputStream inputStream = null; FileOutputStream outputStream = null; FileChannel inChannel = null, outChannel = null; try { inputStream = new FileInputStream(inFile); outputStream = new FileOutputStream(outFile); inChannel = inputStream.getChannel(); outChannel = outputStream.getChannel(); inChannel.transferTo(0, inFile.length(), outChannel); } catch (IOException e) { e.printStackTrace(); return false; } finally { close(outChannel, inChannel, outputStream, inputStream); } return true; } /** * 关闭资源 * * @param closeable 资源 */ public void close(Closeable... closeable) { if (closeable == null) { return; } for (Closeable c : closeable) { if (c != null) { try { c.close(); } catch (IOException e) { e.printStackTrace(); } } } } private void callback(String text) { if (callback != null) { callback.receiver(text); } } }