package com.github.nukc.plugin.helper; import com.android.apksig.apk.ApkFormatException; import com.github.nukc.plugin.axml.ChannelEditor; import com.github.nukc.plugin.axml.decode.AXMLDoc; import com.github.nukc.plugin.model.Options; import com.github.nukc.plugin.ui.OptionForm; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import java.io.*; import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by Nukc. */ public class BuildHelper { private static final Logger log = Logger.getInstance(BuildHelper.class); public static void build(Project project, VirtualFile virtualFile) { if (virtualFile == null) return; Options options = OptionsHelper.load(project); if (options == null) { OptionForm.show(project, virtualFile); return; } if (!OptionsHelper.verify(options)) { OptionForm.show(project, virtualFile); return; } String apkNameWithoutExtension = virtualFile.getNameWithoutExtension(); File apkFile = new File(virtualFile.getPath()); File parentFile = apkFile.getParentFile(); String tempPath = parentFile + File.separator + "temp"; String outPath = parentFile + File.separator + "channels"; ProgressManager.getInstance().run(new Task.Backgroundable(project, "Build Channel") { @Override public void run(@NotNull ProgressIndicator progressIndicator) { log.info("build type -> " + options.buildType); if (OptionsHelper.BUILD_TYPE_UPDATE.equals(options.buildType)) { updateAndroidManifestXml(progressIndicator, virtualFile, tempPath, options, apkNameWithoutExtension, outPath, apkFile); } else if (OptionsHelper.BUILD_TYPE_ADD.equals(options.buildType)) { addChannelFileToMETAINF(progressIndicator, options, apkFile, outPath, tempPath, apkNameWithoutExtension); } else { writeApkComment(progressIndicator, options, apkFile, outPath, tempPath, apkNameWithoutExtension); } } }); } private static void updateAndroidManifestXml(ProgressIndicator progressIndicator, VirtualFile virtualFile, String tempPath, Options options, String apkNameWithoutExtension, String outPath, File apkFile) { List<File> tempApkList = new ArrayList<>(); try { ZipHelper.extractAndroidManifestXml(virtualFile.getPath(), tempPath); String xmlPath = tempPath + File.separator + "AndroidManifest.xml"; AXMLDoc doc = new AXMLDoc(); doc.parse(new FileInputStream(xmlPath)); ChannelEditor editor = new ChannelEditor(doc); for (int i = 0, count = options.channels.size(); i < count; i++) { String channel = options.channels.get(i); progressIndicator.setText("Creating " + channel + " apk"); progressIndicator.setText2(i + 1 + "/" + count); editor.setChannel(channel); editor.commit(); File xmlFile = new File(xmlPath); doc.build(new FileOutputStream(xmlFile)); String channelApkName = apkNameWithoutExtension + "-" + channel + "-unsigned"; String tempApkPath = outPath + File.separator + channelApkName + ".apk"; File tempApk = new File(tempApkPath); FileUtil.createIfDoesntExist(tempApk); tempApkList.add(tempApk); Map<String, File> relpathToFile = new HashMap<>(); relpathToFile.put("AndroidManifest.xml", xmlFile); boolean success = ZipHelper.update(new FileInputStream(apkFile), new FileOutputStream(tempApk), relpathToFile); if (success) { String apkPath = outPath + File.separator + apkNameWithoutExtension + "-" + channel + ".apk"; if ("jarsigner".equals(options.signer)) { CommandHelper.execSigner(options, tempApkPath); CommandHelper.execZipalign(options, tempApkPath, apkPath); } else { CommandHelper.execZipalign(options, tempApkPath, apkPath); CommandHelper.execSigner(options, apkPath); } } else { FileUtil.delete(tempApk); } } } catch (Exception ex) { ex.printStackTrace(); progressIndicator.setText("Build failed"); } finally { progressIndicator.setText("Delete temp"); for (File tempApk : tempApkList) { if (tempApk != null && tempApk.exists()) { tempApk.delete(); } } ZipHelper.deleteTemp(tempPath); progressIndicator.setFraction(1); } } private static void addChannelFileToMETAINF(ProgressIndicator progressIndicator, Options options, File apkFile, String outPath, String tempPath, String apkNameWithoutExtension) { try { final int signVersion = ApkHelper.checkSignatureVersion(apkFile); // 使用 jarsigner 可以先签名后添加渠道空文件 if (signVersion == 0 && options.signer.equals("jarsigner")) { log.warn("the apk is no signature ~"); String tempApkPath = createNotSignApk(apkFile, tempPath, apkNameWithoutExtension); CommandHelper.execSigner(options, tempApkPath); String zipalignApkPath = tempPath + File.separator + apkNameWithoutExtension + ".apk"; CommandHelper.execZipalign(options, tempApkPath, zipalignApkPath); apkFile = new File(zipalignApkPath); } boolean shouldV2Sign = signVersion != 1 && !options.signer.equals("jarsigner"); for (int i = 0, count = options.channels.size(); i < count; i++) { String channel = options.channels.get(i); progressIndicator.setText("Creating " + channel + " apk"); progressIndicator.setText2(i + 1 + "/" + count); String newApkPath; if (shouldV2Sign) { newApkPath = outPath + File.separator + apkNameWithoutExtension + "-" + channel + "-unsigned.apk"; } else { newApkPath = outPath + File.separator + apkNameWithoutExtension + "-" + channel + ".apk"; } File newApkFile = new File(newApkPath); FileUtil.createIfDoesntExist(newApkFile); FileUtil.copy(apkFile, newApkFile); boolean success = updateChannel(newApkPath, channel); if (!success) { FileUtil.delete(newApkFile); } // 使用 apksigner 需要最后重新签名 if (shouldV2Sign) { String zipalignApkPath = outPath + File.separator + apkNameWithoutExtension+ "-" + channel + ".apk"; CommandHelper.execZipalign(options, newApkPath, zipalignApkPath); CommandHelper.execSigner(options, zipalignApkPath); FileUtil.delete(newApkFile); } } ZipHelper.deleteTemp(tempPath); } catch (IOException | NoSuchAlgorithmException |ApkFormatException e) { e.printStackTrace(); progressIndicator.setText("Build failed"); } progressIndicator.setFraction(1); } private static final String CHANNEL_FILE_PREFIX = "c_"; /** * add channel file to META-INF */ private static boolean updateChannel(String newApkPath, String channel) { Path path = Paths.get(newApkPath); URI uri = URI.create("jar:file:" + path.toUri().getPath()); Map<String, String> env = new HashMap<>(); try (FileSystem fileSystem = FileSystems.newFileSystem(uri, env)) { String relPath = "META-INF" + File.separator; final Path root = fileSystem.getPath(relPath); ChannelFileVisitor visitor = new ChannelFileVisitor(); try { Files.walkFileTree(root, visitor); } catch (IOException e) { e.printStackTrace(); log.error("add channel failed:" + channel); return false; } Path existChannel = visitor.getChannelFile(); if (existChannel != null) { Files.delete(existChannel); log.warn("the apk already exists channel:" + existChannel.getFileName().toString() + ", FilePath: " + newApkPath); } String relChannelPath = relPath + CHANNEL_FILE_PREFIX + channel; Path newChannel = fileSystem.getPath(relChannelPath); try { Files.createFile(newChannel); } catch (IOException e) { NotificationHelper.warn("add channel failed"); log.error("add channel failed:" + channel); e.printStackTrace(); return false; } return true; } catch (IOException e) { log.error("add channel failed:" + channel); e.printStackTrace(); return false; } } private static class ChannelFileVisitor extends SimpleFileVisitor<Path> { private Path channelFile; public Path getChannelFile() { return channelFile; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.getFileName().toString().startsWith(CHANNEL_FILE_PREFIX)) { channelFile = file; return FileVisitResult.TERMINATE; } else { return FileVisitResult.CONTINUE; } } } private static void writeApkComment(ProgressIndicator progressIndicator, Options options, File apkFile, String outPath, String tempPath, String apkNameWithoutExtension) { try { if (ZipHelper.hasCommentSign(apkFile)) { String comment = ZipHelper.readZipComment(apkFile); NotificationHelper.error("the apk comment already exists, the comment is " + comment); return; } if (ApkHelper.isV2Signature(apkFile)) { log.info("the apk is v2 signature"); String tempApkPath = createNotSignApk(apkFile, tempPath, apkNameWithoutExtension); //apkSigner is not support write zip comment options.signer = "jarsigner"; CommandHelper.execSigner(options, tempApkPath); String zipalignApkPath = tempPath + File.separator + apkNameWithoutExtension + ".apk"; CommandHelper.execZipalign(options, tempApkPath, zipalignApkPath); apkFile = new File(zipalignApkPath); } for (int i = 0, count = options.channels.size(); i < count; i++) { String channel = options.channels.get(i); progressIndicator.setText("Creating " + channel + " apk"); progressIndicator.setText2(i + 1 + "/" + count); String newApkPath = outPath + File.separator + apkNameWithoutExtension + "-" + channel + ".apk"; File newApkFile = new File(newApkPath); FileUtil.createIfDoesntExist(newApkFile); FileUtil.copy(apkFile, newApkFile); ZipHelper.writeComment(newApkFile, channel); } } catch (IOException | ApkFormatException | NoSuchAlgorithmException e) { e.printStackTrace(); progressIndicator.setText("Build failed"); } finally { ZipHelper.deleteTemp(tempPath); } progressIndicator.setFraction(1); } private static String createNotSignApk(File apkFile, String tempPath, String apkNameWithoutExtension) throws FileNotFoundException { String tempApkPath = tempPath + File.separator + apkNameWithoutExtension + "-unsigned.apk"; File tempApk = new File(tempApkPath); FileUtil.createIfDoesntExist(tempApk); //delete signature boolean success = ZipHelper.update(new FileInputStream(apkFile), new FileOutputStream(tempApk), new HashMap<>(0)); if (!success) { String message = "create tempApk failed, please try again"; NotificationHelper.error(message); throw new RuntimeException(message); } return tempApkPath; } }