diff --git a/.gitignore b/.gitignore
index b075fd30..74773ff1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,16 @@
/local.properties
/.idea/workspace.xml
/.idea/libraries
+/.idea/misc.xml
.DS_Store
/build
build
+.idea/
/captures
.settings
+build-cache
+build-cache.lock
+/dump.txt
+dump.txt
+.cxx
+.externalNativeBuild
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index 60f31796..00000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-Android-Plugin-Framework
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index 96cc43ef..00000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
deleted file mode 100644
index e7bedf33..00000000
--- a/.idea/copyright/profiles_settings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index ce1721d8..00000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 615128f6..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index b0d1ceb8..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 7f68460d..00000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1ddf..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android-Plugin-Framework.iml b/Android-Plugin-Framework.iml
deleted file mode 100644
index 1948fbaf..00000000
--- a/Android-Plugin-Framework.iml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/FairyPlugin/.gitignore b/FairyPlugin/.gitignore
new file mode 100644
index 00000000..9bfafdc9
--- /dev/null
+++ b/FairyPlugin/.gitignore
@@ -0,0 +1,3 @@
+/build
+local.properties
+/*.iml
\ No newline at end of file
diff --git a/FairyPlugin/agp2_3_3/host.gradle b/FairyPlugin/agp2_3_3/host.gradle
new file mode 100644
index 00000000..3361ee23
--- /dev/null
+++ b/FairyPlugin/agp2_3_3/host.gradle
@@ -0,0 +1,820 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+//在宿主脚本中apply此脚本, 此脚本一共做了6件事
+//1、编译完成后导出所有资源id,即aaptOptions.additionalParameters这个配置干的事情, 下面第4步需要此文件
+//2、编译宿主资源时插入public.xml,用来控制宿主资源id分组
+//3、指定插件进程名称,导出.ap_文件
+//4、编译完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包
+//5、编译完成后导出宿主的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时需要此包
+//6、编译完成后导出宿主混淆后的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时若插件需要混淆则需要此包
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+configurations {
+ innerPlugin
+}
+
+def AndroidGradlePluginVersion = getPlugins().findPlugin("com.android.application").androidBuilder.createdBy
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+//第1件事
+android.aaptOptions.additionalParameters("-P", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ configurations.innerPlugin.files.each { file ->
+ //收集插件文件地址
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName
+
+ def scope = variant.getVariantData().getScope()
+ def mergeTask = tasks.getByName(scope.getMergeResourcesTask().name)
+
+ //第2件事
+ mergeTask.doLast {
+ String destPath = mergeTask.outputDir.absolutePath + '/values/';
+ //如果是通过本地路径引入的此脚本
+ if (buildscript.sourceFile != null) {
+
+ def publicXmlPath = buildscript.sourceFile.getParentFile().absolutePath + '/public.xml'
+ println '编译宿主资源时插入' + publicXmlPath + ' 到 ' + destPath + ', 用来控制宿主资源id分组'
+
+ if(!file(publicXmlPath).exists()) {
+ throw new FileNotFoundException("public.xml 文件未找到,请检查路径:" + (buildscript.sourceFile.getParentFile().absolutePath + '/public.xml'))
+ }
+
+ copy {
+ from(buildscript.sourceFile.getParentFile()) {
+ include 'public.xml'
+ }
+ into(destPath)
+ }
+ } else {
+ String url = buildscript.sourceURI.toString().replaceFirst("[a-zA-Z\\.]*\$", "public.xml")
+ println '编译宿主资源时插入' + url + ' 到' + destPath + ', 用来控制宿主资源id分组'
+ HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection())
+ InputStream inputStream = httpConn.getInputStream()
+ OutputStream ouput =new FileOutputStream(new File(destPath, "public.xml"))
+ byte[] buffer = new byte[8*1024]
+ int size = -1
+ while((size = inputStream.read(buffer)) != -1) {
+ ouput.write(buffer, 0, size)
+ }
+ ouput.close()
+ httpConn.disconnect()
+ }
+
+ if (!file(destPath + "public.xml").exists()) {
+ throw new FileNotFoundException("文件不存在:" + destPath + "public.xml")
+ }
+ }
+
+ def mergeAssetsTask = tasks.getByName(scope.getMergeAssetsTask().name)
+ mergeAssetsTask.setOnlyIf { true }
+ mergeAssetsTask.outputs.upToDateWhen { false }
+ mergeAssetsTask.doLast {
+
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ println '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + mergeAssetsTask.outputDir
+ from files(innerPluginApkList)
+ into(mergeAssetsTask.outputDir)
+ }
+ }
+
+ //第3件事
+ for (baseVariant in variant.outputs) {
+
+ def manifestFilePath = baseVariant.processResources.manifestFile.absolutePath;
+
+ baseVariant.processManifest.doLast {
+
+ //android gradle3.+的属性
+ //println "$manifestOutputDirectory"
+
+ File manifestFile = new File(manifestFilePath)
+
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+
+ //实际上最新版sdk和gradle可以直接指定apk了, 不需要.ap_文件
+ def processResourcesTask = baseVariant.getProcessResources();
+ //def processResFullName = baseVariant.apkData.fullName
+ processResourcesTask.doLast {
+ copy {
+ println '编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+
+ //println com.android.builder.Version.ANDROID_GRADLE_PLUGIN_VERSION
+ //Android Gradle Plugin Version 3.x
+ if (!AndroidGradlePluginVersion.startsWith("Android Gradle 2")) {
+ println processResourcesTask.resPackageOutputFolder
+ from processResourcesTask.resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ } else {
+ //Android Gradle Plugin Version 2.x
+ println processResourcesTask.packageOutputFile
+ from processResourcesTask.packageOutputFile
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+ }
+ }
+
+ //第5件事
+ def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile;
+ javaCompile.doLast {
+
+ println "Merge Jar After Task " + javaCompile.name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ javaCompile.classpath.each { jarPath ->
+ jarMerger.addJar(jarPath);
+ //jarMerger.addFolder(directoryInput.getFile());
+ }
+
+ String buildClassesTaskName1 = "jar" + varName.capitalize() + "Classes";
+ String classesPath1 = 'intermediates/packaged/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar";
+
+ String buildClassesTaskName2 = "bundleAppClasses" + varName.capitalize();
+ String classesPath2 = 'intermediates/classes-jar/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar"
+
+ def buildClassesTaskName;
+ def classesPath;
+
+ if (tasks.findByName(buildClassesTaskName1)) {
+ buildClassesTaskName = buildClassesTaskName1;
+ classesPath = classesPath1;
+ } else if (tasks.findByName(buildClassesTaskName2)) {
+ buildClassesTaskName = buildClassesTaskName2;
+ classesPath = classesPath2;
+ } else {
+ throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本")
+ }
+
+ File classes = new File(buildDir, classesPath);
+
+ println "classes path is " + classes.absolutePath
+ println "buildClassesTaskName is " + buildClassesTaskName
+
+ if (!classes.exists()) {
+ try {
+ tasks.getByName(buildClassesTaskName).execute()
+ } catch(Exception e) {
+ println "fail to create jar for task " + javaCompile.name + " " + buildClassesTaskName
+ }
+ } else {
+ println "classes path already exists: " + classes.absolutePath
+ }
+ if (classes.exists()) {
+ jarMerger.addJar(classes)
+ }
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+
+ //第6件事
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}")
+ if (proguardTask) {
+ proguardTask.doFirst {
+ println "开始混淆任务:" + varName.capitalize()
+ }
+ proguardTask.doLast {
+ println "混淆完成:" + varName.capitalize()
+ boolean isFind = false;
+ proguardTask.outputs.files.files.each { File file->
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ project.logger.error "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+
+ isFind = true;
+ def sourceHostObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ def hostObfuscatedJar = "host_obfuscated.jar"
+ println "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar
+
+ copy {
+ from sourceHostObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return hostObfuscatedJar
+ }
+ })
+ }
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+
+ //第4件事
+ tasks.findByName("generate${varName.capitalize()}Sources").doFirst {
+
+ println '编译宿主ap完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 flavorName = ' + flavorName + ", buildTypeName = " + buildTypeName
+
+ createThemePatch(flavorName, buildTypeName);
+
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+task makeHostBaseLine(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ from zipTree("build/outputs/resources.ap_")
+ from('build/outputs') {
+ exclude '*.ap_'
+ }
+}
+
+//导出主题patch
+def createThemePatch(String flavor, String buildType) {
+
+ File patchDir = new File(project.buildDir.absolutePath + "/outputs/theme_patch/" + buildType);
+ patchDir.mkdirs();
+
+ File generatedRes = new File(project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml");
+ File dest = new File(patchDir, "patch_theme.xml")
+
+ println "export from " + generatedRes + " to " + dest
+
+ if (!generatedRes.exists()) {
+ throw new FileNotFoundException("File Not Found : " + generatedRes.absolutePath)
+ }
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ ThemeProcessor.exportThemeStyle(generatedRes, dest, packageName)
+
+ String mergedResDir = "${project.buildDir}/intermediates/res/merged/" + (flavor.equals("")?"":(flavor + "/")) + buildType + "/";
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml', 'values-v1*/values-v1*.xml', 'values-v2*/values-v2*.xml', 'values-*-v1*/values-*-v1*.xml', 'values-*-v4/values-*-v4.xml', 'values-land/values-land.xml', 'values-*-v2*/values-*-v2*.xml', 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ dest = new File(patchDir, 'patch_' + itemFile.name)
+
+ println "export from " + itemFile + " to " + dest
+
+ ThemeProcessor.exportThemeStyle(itemFile, dest, packageName)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ }
+ } else {
+ outXmlStream.write(tag);
+ }
+ } else {
+ outXmlStream.write(tag);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+
+ this.jarOutputStream.putNextEntry(newEntry);
+
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FairyPlugin/agp2_3_3/plugin.gradle b/FairyPlugin/agp2_3_3/plugin.gradle
new file mode 100644
index 00000000..bd476f65
--- /dev/null
+++ b/FairyPlugin/agp2_3_3/plugin.gradle
@@ -0,0 +1,1004 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.zip.*
+
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+//在插件脚本中apply此脚本, 此脚本一共做了3件事
+//1、编译非独立插件时引入编译宿主时导出的资源包.ap_
+//2、编译非独立插件时引入编译宿主时导出的主题包
+//3、扁平处理declare-style,使非独立插件支持declare-style配置
+//4、修正layout文件中的自定义属性的枚举用法
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+
+configurations {
+ baselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+//基线包路径
+def host_patch_path = ""
+//基线包解压路径
+def host_patch_extract_dir = project.buildDir.absolutePath + File.separator + 'outputs' + File.separator + 'baselinePatch'
+def hostClassesJar = host_patch_extract_dir + File.separator + "host_classes.jar"
+
+//基线包文件名
+def host_patch_fileName = "host.bar"
+
+
+def ApplicationPlugin = getPlugins().findPlugin("com.android.application")
+def LibraryPlugin = getPlugins().findPlugin("com.android.library")
+def isApplicationModule
+def AndroidGradlePluginVersion
+if (ApplicationPlugin != null) {
+ isApplicationModule = true;
+ AndroidGradlePluginVersion = ApplicationPlugin.androidBuilder.createdBy
+} else {
+ isApplicationModule = false;
+ AndroidGradlePluginVersion = LibraryPlugin.androidBuilder.createdBy
+}
+def fairyMinifyEnabled = false
+
+android{
+}
+
+dependencies {
+
+ if (AndroidGradlePluginVersion.startsWith("Android Gradle 2")) {
+ if (!fairyMinifyEnabled) {
+ provided files(hostClassesJar)
+ } else {
+ compile files(hostClassesJar)
+ }
+ }
+
+}
+
+//第一件事 将宿主资源添加到编译时,仅参与编译,不参与打包
+android.aaptOptions.additionalParameters('-I', host_patch_extract_dir + "/" + host_patch_fileName)
+
+HashMap>> declareStyleMap = new HashMap>>();
+
+afterEvaluate {
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+
+ configurations.each { config ->
+ if (config.name.equals("baselinePatch")) {
+ config.files.each { patch ->
+
+ //读取宿主基线包文件路径
+ host_patch_path = patch.absolutePath;
+ println project.name + "发现宿主基线配置指向位置:" + host_patch_path
+
+ if (!AndroidGradlePluginVersion.startsWith("Android Gradle 2")) {
+
+ //读取混淆开关配置
+ gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.startsWith("assemble")) {
+ if (startTaskName.contains("Debug")) {
+ fairyMinifyEnabled = android.buildTypes.debug.minifyEnabled
+ } else if (startTaskName.contains("Release")) {
+ fairyMinifyEnabled = android.buildTypes.release.minifyEnabled
+ }
+ }
+ }
+
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ println project.name + "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + fairyMinifyEnabled
+ if (!fairyMinifyEnabled) {
+ configurations.compileOnly.dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations.implementation.dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+ }
+ }
+ }
+ }
+
+ def moduleVariants = isApplicationModule?android.applicationVariants:android.libraryVariants
+ for (variant in moduleVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName
+
+ def generateDebugSourcesTask = tasks.findByName("generate${varName.capitalize()}Resources")
+ if (generateDebugSourcesTask) {
+ generateDebugSourcesTask.setOnlyIf { true }
+ generateDebugSourcesTask.outputs.upToDateWhen { false }
+ generateDebugSourcesTask.doFirst {
+
+ println "检查宿主基线patch文件:" + host_patch_path
+
+ if (!file(host_patch_path).exists()) {
+ file(host_patch_extract_dir).deleteDir();
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path)
+ }
+ copy {
+ println "正在解压宿主基线patch文件:" + host_patch_path
+ def zipFile = file(host_patch_path);//host.bar
+ def outputDir = file(host_patch_extract_dir)//outputs/baselinePatch
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ host_patch_fileName
+ }
+ }
+ into outputDir
+ }
+ }
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+
+ println "开始merge插件工程资源:" + host_patch_path
+
+ if (!file(host_patch_path).exists()) {
+ file(host_patch_extract_dir).deleteDir();
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path)
+ }
+
+ File propFile = file(host_patch_extract_dir + "/HostInfo.prop");
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ def hostBuildType = props.getProperty("host.buildType")
+
+ //第二件事
+ applyThemePatch(hostBuildType, buildTypeName, host_patch_extract_dir);
+
+ //第三件事<一>
+ File originVaules = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName + "/values/values.xml");
+ if (originVaules.exists()) {
+
+ File valuesProcessed = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName + "/values/values_processed.xml");
+
+ println "fixDeclareStyle " + originVaules + ", " + valuesProcessed.absolutePath
+
+ StyleProcessor styleProcessor = StyleProcessor.fixDeclareStyle(originVaules, valuesProcessed);
+
+ SortedMap> map = styleProcessor.attrList;
+ if (map != null) {
+ declareStyleMap.put(buildTypeName, map)
+ }
+ originVaules.delete()
+
+ //第四件事,修正layout文件中的自定义属性的枚举用法
+ File resDir = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName);
+ resDir.eachFileRecurse({file ->
+ if (file.absolutePath.contains("/layout") && file.name.endsWith(".xml")) {
+ String fileText = file.text;
+ Iterator> itr = styleProcessor.enumItemList.iterator();
+ println "try fix enum attr for " + file.absolutePath
+ while(itr.hasNext()) {
+ Map.Entry entryItem = itr.next();
+ fileText = fileText.replaceAll(entryItem.key, entryItem.value)
+ }
+
+ println "try fix flag attr for " + file.absolutePath
+ fileText = StyleProcessor.fixAttrFlag(fileText, styleProcessor.attrFlagMap);
+
+ file.write(fileText);
+ }
+ }
+ )
+ } else {
+ println "没有需要处理的DeclareStyle资源"
+ }
+ }
+
+ def generateSourcesTask = tasks.findByName("generate${varName.capitalize()}Sources");
+ generateSourcesTask.doLast {
+ //第三件事<二>
+ FileTree rfiles = fileTree(dir: project.buildDir.absolutePath + "/generated/source/r/"+ buildTypeName, include: ['**/R.java'])
+ rfiles.each { File itemFile ->
+
+ File newR = new File(itemFile.getAbsolutePath()+ "_temp");
+ File originR = itemFile;
+
+ println "处理R.java文件,生成decalre-style , \n" + newR.absolutePath + ", \n" + originR.absolutePath
+
+ newR.delete()
+ SortedMap> map = declareStyleMap.get(buildTypeName);
+
+ if (originR.exists() && map != null) {
+ BufferedReader br = new BufferedReader(new FileReader(originR));
+ BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newR), "UTF-8"));
+ String srcLine = null;
+ boolean hasStyleable = false;
+ String packageStr = "";
+ while((srcLine = br.readLine()) != null) {
+ if (srcLine.equals("public final class R {")) {
+ bw.writeLine(srcLine);
+
+ bw.writeLine(" public static final class styleable {");
+ Iterator>> itr = map.iterator();
+ while(itr.hasNext()) {
+
+ Map.Entry> item = itr.next();
+ String key = item.key;
+ StringBuilder sb = new StringBuilder();
+
+ println "Generate styleable " + key
+
+ for(int i = 0; i < item.value.size(); i++) {
+ bw.writeLine(" public static int " + key + "_" + item.value.get(i) + "=" + i + ";");
+ sb.append(packageStr).append(".R.attr.").append(item.value.get(i)).append(",")
+ }
+ if (sb.length() > 0) {
+ sb.deleteCharAt(sb.length()-1)
+ }
+ bw.writeLine(" public static int[] " + key + " = {" + sb.toString() + "};");
+ }
+ bw.writeLine(" }")
+ } else if (srcLine.contains("public static final class styleable {")) {
+ hasStyleable = true;
+ } else if (hasStyleable && srcLine.trim().equals("}")) {
+ hasStyleable = false;
+ } else {
+ if (srcLine.startsWith("package ") && srcLine.endsWith(";")) {
+ if(isApplicationModule) {
+ packageStr = android.defaultConfig.applicationId
+ } else {
+ packageStr = srcLine.replace("package ", "").replace(";", "");
+ }
+ }
+ bw.writeLine(srcLine);
+ }
+ }
+ bw.close()
+ br.close()
+
+ originR.delete()
+ newR.renameTo(originR)
+ } else {
+ println "Nothing to Fix for " + originR.absolutePath
+ }
+ }
+ }
+
+ if (isApplicationModule) {
+ for (baseVariant in variant.outputs) {
+
+ baseVariant.processResources.doFirst {
+ println "开始处理插件工程资源.."
+ if (!new File(host_patch_path).exists()) {
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path)
+ }
+ if (!file(host_patch_extract_dir).exists()) {
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_extract_dir)
+ }
+ }
+
+ baseVariant.processManifest.doLast {
+ File propFile = file(host_patch_extract_dir + "/HostInfo.prop");
+ def hostVersionName = null
+ def hostVersionCode = null
+ def hostApplicationId = null
+ def hostBuildType = null
+ def hostFlavorName = null
+ if (propFile.exists()) {
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ hostVersionName = props.getProperty("host.versionName")
+ hostVersionCode = props.getProperty("host.versionCode")
+ hostBuildType = props.getProperty("host.buildType")
+ hostFlavorName = props.getProperty("host.flavorName")
+ hostApplicationId = props.getProperty("host.applicationId")
+
+ println "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName
+
+ } else {
+ throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath)
+ }
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest文件中插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插件Manifest文件中插入非独立插件标识 requiredHostApplicationId=" + hostApplicationId
+ def manifestFile
+ if (AndroidGradlePluginVersion.startsWith("Android Gradle 2")) {
+ manifestFile = new File(properties.get("manifestOutputFile").absolutePath)
+ } else {
+ manifestFile = new File("$manifestOutputDirectory/AndroidManifest.xml")
+ }
+ println "插件Manifest:" + manifestFile.absolutePath
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ println "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+ isFind = true;
+ def pluginObfuscatedJar = "plugin_obfuscated.jar"
+ def sourcePluginObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ //保留一个备份
+ println "导出混淆后的插件jar包 "
+ println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar
+ println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar
+ copy {
+ from sourcePluginObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return pluginObfuscatedJar
+ }
+ })
+ }
+
+ diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar);
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+ }
+ }
+}
+
+def diffJar(String plugin, String host) {
+
+
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+
+ println "插件:" + plugin + ", 宿主:" + host
+
+ println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/"
+
+ ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/")
+ List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/")
+
+ println "再删掉插件的源 " + plugin
+ new File(plugin).delete()
+
+ println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName()
+ ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses)
+ println "重新生成的插件的源 " + plugin
+
+ //备份diff后的包
+ copy {
+ println "备份diff后重新生成的插件的源包到插件out目录"
+ println "From " + plugin
+ println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar"
+ from plugin
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return "plugin_obfuscated_after_diff.jar"
+ }
+ })
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
+
+def applyThemePatch(hostBuildType, buildTypeName, hostPatchExtractDir) {
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + hostBuildType;
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/res/merged/" + buildTypeName;
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + resourceDir);
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+
+ String dest;
+ if (itemFile.name.equals('patch_theme.xml')) {
+ dest = resourceDir + '/values/'
+ } else {
+ dest = resourceDir + '/' + itemFile.name.replace('patch_', '').replace('.xml', '') + "/"
+ }
+
+ println "apply theme_patch from " + itemFile + " to " + dest
+ copy {
+ from(fromDir) {
+ include itemFile.name
+ }
+ into(dest)
+ }
+ }
+}
+
+/** fix provided config for aar dependences
+ ext.plist = []
+ configurations.provided.dependencies.each {dep ->
+ println dep
+ gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.startsWith("assemble") || startTaskName.startsWith("build")) {
+ if (dep.version.equals('unspecified')) {
+
+ String buildType = startTaskName.replace("assemble", "").replace("build", "")
+ if (buildType.length() == 0) {
+ buildType = "debug";
+ }
+
+ //为依赖库插入jar任务
+ dep.dependencyProject.tasks.getByName("compile" + buildType + "Sources").doLast {
+ String jarTaskName = "jar" + buildType.substring(0, 1).toUpperCase()+ buildType.replaceFirst("\\w","") + "Classes"
+ println '执行task:' + jarTaskName;
+ dep.dependencyProject.tasks.getByName(jarTaskName).execute()
+ }
+
+ //src的jar
+ File srcJar = new File(dep.dependencyProject.buildDir, "intermediates/packaged/" + buildType + "/classes.jar")
+ addProvidedDependences(configurations.provided.dependencies, srcJar);
+
+ //处理aar依赖
+ File aarDir = new File(dep.dependencyProject.buildDir, "intermediates/exploded-aar")
+ addProvidedDependences(configurations.provided.dependencies, aarDir)
+
+ //处理libs/jar依赖
+ //TODO
+
+ }
+ }
+ }
+ }
+
+ configurations.provided.dependencies.clear()
+
+ ext.plist.each { path ->
+ configurations.provided.dependencies.add(project.dependencies.create(files(path)))
+ println "try add provided jar to plugin project : " + path
+ }
+
+ def addProvidedDependences(DependencySet providedDepSet, File root) {
+ //宿主编译前文件可能不存在, 所以要先编译宿主 再编译插件
+ if (root.getName().endsWith(".jar")) {
+ ext.plist +=[root.getAbsolutePath()]
+ //providedDepSet.add(project.dependencies.create(files(root.getAbsolutePath())))
+ //println "try add provided jar to plugin project : " + root.getAbsolutePath()
+ } else {
+ File[] subFiles = root.listFiles();
+ if (subFiles != null) {
+ for (File subFile : subFiles) {
+ addProvidedDependences(providedDepSet, subFile);
+ }
+ }
+ }
+ }
+ */
+
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
\ No newline at end of file
diff --git a/PluginMain/public.xml b/FairyPlugin/agp2_3_3/public.xml
similarity index 70%
rename from PluginMain/public.xml
rename to FairyPlugin/agp2_3_3/public.xml
index 52cb34df..7316a33f 100644
--- a/PluginMain/public.xml
+++ b/FairyPlugin/agp2_3_3/public.xml
@@ -16,7 +16,7 @@
-
+
@@ -24,4 +24,12 @@
+
+
+
+
+
+
+
+
diff --git a/FairyPlugin/agp3_0_1/host.gradle b/FairyPlugin/agp3_0_1/host.gradle
new file mode 100644
index 00000000..ef3e4be8
--- /dev/null
+++ b/FairyPlugin/agp3_0_1/host.gradle
@@ -0,0 +1,816 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+//在宿主脚本中apply此脚本, 此脚本一共做了6件事
+//1、编译完成后导出所有资源id,即aaptOptions.additionalParameters这个配置干的事情, 下面第4步需要此文件
+//2、编译宿主资源时插入public.xml,用来控制宿主资源id分组
+//3、指定插件进程名称,导出.ap_文件
+//4、编译完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包
+//5、编译完成后导出宿主的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时需要此包
+//6、编译完成后导出宿主混淆后的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时若插件需要混淆则需要此包
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////
+configurations {
+ innerPlugin
+}
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+//第1件事
+android.aaptOptions.additionalParameters("-P", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ configurations.innerPlugin.files.each { file ->
+ //收集插件文件地址
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName
+
+ def scope = variant.getVariantData().getScope()
+ def mergeTask = tasks.getByName(scope.getMergeResourcesTask().name)
+
+ //第2件事
+ mergeTask.doLast {
+ String destPath = mergeTask.outputDir.absolutePath + '/values/';
+ //如果是通过本地路径引入的此脚本
+ if (buildscript.sourceFile != null) {
+
+ def publicXmlPath = buildscript.sourceFile.getParentFile().absolutePath + '/public.xml'
+ println '编译宿主资源时插入' + publicXmlPath + ' 到 ' + destPath + ', 用来控制宿主资源id分组'
+
+ if(!file(publicXmlPath).exists()) {
+ throw new FileNotFoundException("public.xml 文件未找到,请检查路径:" + (buildscript.sourceFile.getParentFile().absolutePath + '/public.xml'))
+ }
+
+ copy {
+ from(buildscript.sourceFile.getParentFile()) {
+ include 'public.xml'
+ }
+ into(destPath)
+ }
+ } else {
+ String url = buildscript.sourceURI.toString().replaceFirst("[a-zA-Z\\.]*\$", "public.xml")
+ println '编译宿主资源时插入' + url + ' 到' + destPath + ', 用来控制宿主资源id分组'
+ HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection())
+ InputStream inputStream = httpConn.getInputStream()
+ File destPublicXmlFile = new File(destPath, "public.xml")
+ File parentFile = destPublicXmlFile.getParentFile()
+ if (!parentFile.exists()) {
+ parentFile.mkdirs()
+ }
+
+ OutputStream ouput =new FileOutputStream(destPublicXmlFile)
+ byte[] buffer = new byte[8*1024]
+ int size = -1
+ while((size = inputStream.read(buffer)) != -1) {
+ ouput.write(buffer, 0, size)
+ }
+ ouput.close()
+ httpConn.disconnect()
+ }
+
+ if (!file(destPath + "public.xml").exists()) {
+ throw new FileNotFoundException("文件不存在:" + destPath + "public.xml")
+ }
+ }
+
+ def mergeAssetsTask = tasks.getByName(scope.getMergeAssetsTask().name)
+ mergeAssetsTask.setOnlyIf { true }
+ mergeAssetsTask.outputs.upToDateWhen { false }
+ mergeAssetsTask.doLast {
+
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ println '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + mergeAssetsTask.outputDir
+ from files(innerPluginApkList)
+ into(mergeAssetsTask.outputDir)
+ }
+ }
+
+ //第3件事
+ for (baseVariant in variant.outputs) {
+
+ def manifestFilePath = baseVariant.processResources.manifestFile.absolutePath;
+
+ baseVariant.processManifest.doLast {
+
+ //android gradle3.+的属性
+ //println "$manifestOutputDirectory"
+
+ File manifestFile = new File(manifestFilePath)
+
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+
+ //实际上最新版sdk和gradle可以直接指定apk了, 不需要.ap_文件
+ def processResourcesTask = baseVariant.getProcessResources();
+ //def processResFullName = baseVariant.apkData.fullName
+ processResourcesTask.doLast {
+ copy {
+ println '编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+ println processResourcesTask.resPackageOutputFolder
+ from processResourcesTask.resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+ }
+
+ //第5件事
+ def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile;
+ javaCompile.doLast {
+ HashMap jarSets = new HashMap();
+ jarSets.put("android.jar", "")
+
+ println "Merge Jar After Task " + javaCompile.name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ javaCompile.classpath.each { jarPath ->
+ String jarName = jarPath.getName();
+ if(!jarSets.keySet().contains(jarName)) {
+ println "addJar path: $jarPath"
+ jarMerger.addJar(jarPath);
+ if (!jarName.equals("classes.jar") && jarPath.absolutePath.endsWith("/libs/" + jarName)) {
+ jarSets.put(jarName, jarPath.absolutePath)
+ }
+ } else {
+ println "ignore jar: $jarPath \n\\------ because of : " + jarSets.get(jarName)
+ }
+ }
+
+ String buildClassesTaskName1 = "jar" + varName.capitalize() + "Classes";
+ String classesPath1 = 'intermediates/packaged/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar";
+
+ String buildClassesTaskName2 = "bundleAppClasses" + varName.capitalize();
+ String classesPath2 = 'intermediates/classes-jar/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar"
+
+ def buildClassesTaskName;
+ def classesPath;
+
+ if (tasks.findByName(buildClassesTaskName1)) {
+ buildClassesTaskName = buildClassesTaskName1;
+ classesPath = classesPath1;
+ } else if (tasks.findByName(buildClassesTaskName2)) {
+ buildClassesTaskName = buildClassesTaskName2;
+ classesPath = classesPath2;
+ } else {
+ throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本")
+ }
+
+ File classes = new File(buildDir, classesPath);
+
+ println "classes path is " + classes.absolutePath
+ println "buildClassesTaskName is " + buildClassesTaskName
+
+ if (!classes.exists()) {
+ try {
+ tasks.getByName(buildClassesTaskName).execute()
+ } catch(Exception e) {
+ println "fail to create jar for task " + javaCompile.name + " " + buildClassesTaskName
+ }
+ } else {
+ println "classes path already exists: " + classes.absolutePath
+ }
+ if (classes.exists()) {
+ println "addJar path: $classes.absolutePath"
+ jarMerger.addJar(classes)
+ }
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+
+ //第6件事
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}")
+ if (proguardTask) {
+ proguardTask.doFirst {
+ println "开始混淆任务:" + varName.capitalize()
+ }
+ proguardTask.doLast {
+ println "混淆完成:" + varName.capitalize()
+ boolean isFind = false;
+ proguardTask.outputs.files.files.each { File file->
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ project.logger.error "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+
+ isFind = true;
+ def sourceHostObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ def hostObfuscatedJar = "host_obfuscated.jar"
+ println "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar
+
+ copy {
+ from sourceHostObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return hostObfuscatedJar
+ }
+ })
+ }
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+
+ //第4件事
+ tasks.findByName("generate${varName.capitalize()}Sources").doFirst {
+
+ println '编译宿主ap完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 flavorName = ' + flavorName + ", buildTypeName = " + buildTypeName
+
+ createThemePatch(flavorName, buildTypeName);
+
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+task makeHostBaseLine(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ from zipTree("build/outputs/resources.ap_")
+ from('build/outputs') {
+ exclude '*.ap_'
+ }
+}
+
+//导出主题patch
+def createThemePatch(String flavor, String buildType) {
+
+ File patchDir = new File(project.buildDir.absolutePath + "/outputs/theme_patch/" + buildType);
+ patchDir.mkdirs();
+
+ File generatedRes = new File(project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml");
+ File dest = new File(patchDir, "patch_theme.xml")
+
+ println "export from " + generatedRes + " to " + dest
+
+ if (!generatedRes.exists()) {
+ throw new FileNotFoundException("File Not Found : " + generatedRes.absolutePath)
+ }
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ ThemeProcessor.exportThemeStyle(generatedRes, dest, packageName)
+
+ String mergedResDir = "${project.buildDir}/intermediates/res/merged/" + (flavor.equals("")?"":(flavor + "/")) + buildType + "/";
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml', 'values-v1*/values-v1*.xml', 'values-v2*/values-v2*.xml', 'values-*-v1*/values-*-v1*.xml', 'values-*-v4/values-*-v4.xml', 'values-land/values-land.xml', 'values-*-v2*/values-*-v2*.xml', 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ dest = new File(patchDir, 'patch_' + itemFile.name)
+
+ println "export from " + itemFile + " to " + dest
+
+ ThemeProcessor.exportThemeStyle(itemFile, dest, packageName)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ }
+ } else {
+ outXmlStream.write(tag);
+ }
+ } else {
+ outXmlStream.write(tag);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+
+ this.jarOutputStream.putNextEntry(newEntry);
+
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FairyPlugin/agp3_0_1/plugin.gradle b/FairyPlugin/agp3_0_1/plugin.gradle
new file mode 100644
index 00000000..d37dab2e
--- /dev/null
+++ b/FairyPlugin/agp3_0_1/plugin.gradle
@@ -0,0 +1,920 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.zip.*
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.aaptOptions.additionalParameters(
+ '-I', currentSelectedBar)
+
+HashMap>> declareStyleMap = new HashMap>>();
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+
+ println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName
+
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", hostBarExtraRootDir)
+ }
+
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ def hostBuildType = props.getProperty("host.buildType")
+
+ //第二件事
+ applyThemePatch(hostBuildType, varDirName, hostBarExtraRootDir);
+
+ //第三件事<一>
+ File originVaules = new File(project.buildDir, "intermediates/res/merged/" + varDirName + "/values/values.xml");
+ if (originVaules.exists()) {
+
+ File valuesProcessed = new File(project.buildDir, "intermediates/res/merged/" + varDirName + "/values/values_processed.xml");
+
+ println "fixDeclareStyle " + originVaules + ", " + valuesProcessed.absolutePath
+
+ StyleProcessor styleProcessor = StyleProcessor.fixDeclareStyle(originVaules, valuesProcessed);
+
+ SortedMap> map = styleProcessor.attrList;
+ if (map != null) {
+ declareStyleMap.put(varDirName, map)
+ }
+ originVaules.delete()
+
+ //第四件事,修正layout文件中的自定义属性的枚举用法
+ File resDir = new File(project.buildDir, "intermediates/res/merged/" + varDirName);
+ resDir.eachFileRecurse({file ->
+ if (file.absolutePath.contains("/layout") && file.name.endsWith(".xml")) {
+ String fileText = file.text;
+ Iterator> itr = styleProcessor.enumItemList.iterator();
+ println "try fix enum attr for " + file.absolutePath
+ while(itr.hasNext()) {
+ Map.Entry entryItem = itr.next();
+ fileText = fileText.replaceAll(entryItem.key, entryItem.value)
+ }
+
+ println "try fix flag attr for " + file.absolutePath
+ fileText = StyleProcessor.fixAttrFlag(fileText, styleProcessor.attrFlagMap);
+
+ file.write(fileText);
+ }
+ }
+ )
+ } else {
+ println "没有需要处理的DeclareStyle资源"
+ }
+ }
+
+ def generateSourcesTask = tasks.findByName("generate${varName.capitalize()}Sources");
+ generateSourcesTask.doLast {
+ //第三件事<二>
+ FileTree rfiles = fileTree(dir: project.buildDir.absolutePath + "/generated/source/r/"+ varDirName, include: ['**/R.java'])
+ rfiles.each { File itemFile ->
+
+ File newR = new File(itemFile.getAbsolutePath()+ "_temp");
+ File originR = itemFile;
+
+ println "处理R.java文件,生成decalre-style , \n" + newR.absolutePath + ", \n" + originR.absolutePath
+
+ newR.delete()
+ SortedMap> map = declareStyleMap.get(varDirName);
+
+ if (originR.exists() && map != null) {
+ BufferedReader br = new BufferedReader(new FileReader(originR));
+ BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newR), "UTF-8"));
+ String srcLine = null;
+ boolean hasStyleable = false;
+ String packageStr = "";
+ while((srcLine = br.readLine()) != null) {
+ if (srcLine.equals("public final class R {")) {
+ bw.writeLine(srcLine);
+
+ bw.writeLine(" public static final class styleable {");
+ Iterator>> itr = map.iterator();
+ while(itr.hasNext()) {
+
+ Map.Entry> item = itr.next();
+ String key = item.key;
+ StringBuilder sb = new StringBuilder();
+
+ println "Generate styleable " + key
+
+ for(int i = 0; i < item.value.size(); i++) {
+ bw.writeLine(" public static int " + key + "_" + item.value.get(i) + "=" + i + ";");
+ sb.append(packageStr).append(".R.attr.").append(item.value.get(i)).append(",")
+ }
+ if (sb.length() > 0) {
+ sb.deleteCharAt(sb.length()-1)
+ }
+ bw.writeLine(" public static int[] " + key + " = {" + sb.toString() + "};");
+ }
+ bw.writeLine(" }")
+ } else if (srcLine.contains("public static final class styleable {")) {
+ hasStyleable = true;
+ } else if (hasStyleable && srcLine.trim().equals("}")) {
+ hasStyleable = false;
+ } else {
+ if (srcLine.startsWith("package ") && srcLine.endsWith(";")) {
+ if(isApplicationModule) {
+ packageStr = android.defaultConfig.applicationId
+ } else {
+ packageStr = srcLine.replace("package ", "").replace(";", "");
+ }
+ }
+ bw.writeLine(srcLine);
+ }
+ }
+ bw.close()
+ br.close()
+
+ originR.delete()
+ newR.renameTo(originR)
+ } else {
+ println "Nothing to Fix for " + originR.absolutePath
+ }
+ }
+ }
+
+ if (isApplicationModule) {
+ for (baseVariant in variant.outputs) {
+
+ baseVariant.processResources.doFirst {
+ println "开始处理插件工程资源.."
+ if (!file(hostBarExtraRootDir).exists()) {
+ throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", hostBarExtraRootDir)
+ }
+ }
+
+ baseVariant.processManifest.doLast {
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ def hostVersionName = null
+ def hostVersionCode = null
+ def hostApplicationId = null
+ def hostBuildType = null
+ def hostFlavorName = null
+ if (propFile.exists()) {
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ hostVersionName = props.getProperty("host.versionName")
+ hostVersionCode = props.getProperty("host.versionCode")
+ hostBuildType = props.getProperty("host.buildType")
+ hostFlavorName = props.getProperty("host.flavorName")
+ hostApplicationId = props.getProperty("host.applicationId")
+
+ println "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName
+
+ } else {
+ throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath)
+ }
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest文件中插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插件Manifest文件中插入非独立插件标识 requiredHostApplicationId=" + hostApplicationId
+ def manifestFile = new File("$manifestOutputDirectory/AndroidManifest.xml")
+ println "插件Manifest:" + manifestFile.absolutePath
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ println "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+ isFind = true;
+ def pluginObfuscatedJar = "plugin_obfuscated.jar"
+ def sourcePluginObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ //保留一个备份
+ println "导出混淆后的插件jar包 "
+ println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar
+ println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar
+ copy {
+ from sourcePluginObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return pluginObfuscatedJar
+ }
+ })
+ }
+
+ diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar);
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+ }
+ }
+}
+
+def diffJar(String plugin, String host) {
+
+
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+
+ println "插件:" + plugin + ", 宿主:" + host
+
+ println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/"
+
+ ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/")
+ List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/")
+
+ println "再删掉插件的源 " + plugin
+ new File(plugin).delete()
+
+ println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName()
+ ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses)
+ println "重新生成的插件的源 " + plugin
+
+ //备份diff后的包
+ copy {
+ println "备份diff后重新生成的插件的源包到插件out目录"
+ println "From " + plugin
+ println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar"
+ from plugin
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return "plugin_obfuscated_after_diff.jar"
+ }
+ })
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
+
+def applyThemePatch(hostBuildType, varDirName, hostPatchExtractDir) {
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + hostBuildType;
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/res/merged/" + varDirName;
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + resourceDir);
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+
+ String dest;
+ if (itemFile.name.equals('patch_theme.xml')) {
+ dest = resourceDir + '/values/'
+ } else {
+ dest = resourceDir + '/' + itemFile.name.replace('patch_', '').replace('.xml', '') + "/"
+ }
+
+ println "apply theme_patch from " + itemFile + " to " + dest
+ copy {
+ from(fromDir) {
+ include itemFile.name
+ }
+ into(dest)
+ }
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
\ No newline at end of file
diff --git a/FairyPlugin/agp3_0_1/public.xml b/FairyPlugin/agp3_0_1/public.xml
new file mode 100644
index 00000000..7316a33f
--- /dev/null
+++ b/FairyPlugin/agp3_0_1/public.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FairyPlugin/agp3_2_1/host.gradle b/FairyPlugin/agp3_2_1/host.gradle
new file mode 100644
index 00000000..19373463
--- /dev/null
+++ b/FairyPlugin/agp3_2_1/host.gradle
@@ -0,0 +1,858 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+configurations {
+ innerPlugin
+}
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+if ("false".equals(rootProject.properties.get("android.enableAapt2"))) {
+ throw new Exception("请开启aapt2编译开关:android.enableAapt2")
+}
+
+//generateSourcess时借此文件生成attrs.xml
+android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //收集需要内置的插件文件地址
+ configurations.innerPlugin.files.each { file ->
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ variant.getMergeResources().doLast {
+
+ project.logger.error '生成一份主题patch包,编译非独立插件时需要此包'
+
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ patchDir.mkdirs();
+
+ //宿主资源id分组就靠这个文件了
+ def String publicAttrrsXml = "res/values/public_attrs.xml" ;
+ def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath;
+
+ //导出一份主题包备用,编译非独立插件时需要
+ createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile);
+
+ if(!file(publicAttrrsXmlFile).exists()) {
+ throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile)
+ }
+
+ String mergeResourcesOutPath = outputDir.absolutePath;
+
+ //需要确保aapt2在环境变量中,否则可能会执行失败
+ //Android Studio环境中的aapt2和命令行中的可能不是同一个版本,
+ //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2
+ //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2
+ //如果不是同一个版本可能出现Android Resource Link错误、magic number错误
+ //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile")
+ "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor()
+
+ //检查aapt2是否执行成功
+ if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) {
+ throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,请检查路径:$publicAttrrsXmlFile")
+ }
+
+ //生成values_public_attrs这个文件以后,publicAttrrsXmlFile文件已经无用了,
+ //接下来修改内容需要为生成一个public attr
+ String text = file(publicAttrrsXmlFile).text
+ .replace("public_static_final_host", "public_static_final_plugin")
+ .replace("0x7f3", "0x7f0")
+ .replace("0x7f4", "0x7f1")
+ file(publicAttrrsXmlFile).write(text)
+ }
+
+ def mergeAssetsTask = variant.getMergeAssets()
+ mergeAssetsTask.setOnlyIf { true }
+ mergeAssetsTask.outputs.upToDateWhen { false }
+ mergeAssetsTask.doLast {
+
+ //检查内置插件坐标是否存在,不存在给出提示
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir
+ from files(innerPluginApkList)
+ into(outputDir)
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ //没有单独命名,有多个abi时文件会覆盖
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+ hostInfo.append("\nhost.varName=" + varName)
+ hostInfo.append("\nhost.varDirName=" + varDirName)
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Resources").doLast {
+ //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件
+ //def processResFullName = baseVariant.apkData.fullName
+ copy {
+ project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+ println "from :" + resPackageOutputFolder
+ from resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+
+ //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用
+ def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile;
+ javaCompile.doLast {
+
+ println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ classpath.each { jarPath ->
+ println "adding jar " + jarPath
+ jarMerger.addJar(jarPath);
+ //jarMerger.addFolder(directoryInput.getFile());
+ }
+
+ def buildClassesTaskName = "packageAppClasses" + varName.capitalize();
+ def classesPath= 'intermediates/app_classes/' + varName + "/" + buildClassesTaskName + "/classes.jar"
+
+ if (!tasks.findByName(buildClassesTaskName)) {
+ throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本")
+ }
+
+ File classes = new File(buildDir, classesPath);
+
+ if (!classes.exists()) {
+ try {
+ tasks.getByName(buildClassesTaskName).execute()
+ } catch(Exception e) {
+ projects.logger.log(LogLevel.ERROR, "fail to create jar for task " + name + " " + buildClassesTaskName)
+ }
+ } else {
+ projects.logger.log(LogLevel.DEBUG, "classes path already exists: " + classes.absolutePath)
+ }
+ if (classes.exists()) {
+ println "adding jar " + classes
+ jarMerger.addJar(classes)
+ } else {
+ projects.logger.log(LogLevel.ERROR, "Not exists : classes file path is " + classes.absolutePath)
+ projects.logger.log(LogLevel.ERROR, "Not exists : build classes task name is " + buildClassesTaskName)
+ }
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}")
+ if (proguardTask) {
+ proguardTask.doFirst {
+ println "开始混淆任务:" + varName.capitalize()
+ }
+ proguardTask.doLast {
+ println "混淆完成:" + varName.capitalize()
+ boolean isFind = false;
+ proguardTask.outputs.files.files.each { File file->
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ project.logger.error "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+
+ isFind = true;
+ def sourceHostObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ def hostObfuscatedJar = "host_obfuscated.jar"
+ project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar
+
+ copy {
+ from sourceHostObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ project.logger.error "rename:" + s
+ return hostObfuscatedJar
+ }
+ })
+ }
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+
+ //修改public_attrs的内容,填充attr的id
+ tasks.findByName("generate${varName.capitalize()}Sources").doLast {
+ def f = file(buildDir.absolutePath + "/outputs/theme_patch/" + varDirName + "/res/values/public_attrs.xml")
+ project.logger.error "update public attrs ids, file=" + f.absolutePath
+ def String text = f.text
+ if (f.exists()) {
+ file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el->
+ if (el.contains(":attr/")) {
+ String[] attrIdKeyValue = el.split("/")[1].split(" = ");
+ text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">")
+ }
+ }
+ f.write(text)
+ } else {
+ throw new Exception(f.absolutePath + " Not Found!")
+ }
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+task makeHostBaseLine(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ from zipTree("${project.buildDir}/outputs/resources.ap_")
+ from("${project.buildDir}/outputs") {
+ exclude '*.ap_'
+ }
+}
+
+//导出主题patch
+def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) {
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/"
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml',
+ 'values-v1*/values-v1*.xml',
+ 'values-v2*/values-v2*.xml',
+ 'values-*-v1*/values-*-v1*.xml',
+ 'values-*-v4/values-*-v4.xml',
+ 'values-land/values-land.xml',
+ 'values-*-v2*/values-*-v2*.xml',
+ 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "")
+ def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name)
+ destFile.getParentFile().mkdirs()
+ println "export from " + itemFile + " to " + destFile
+
+ //通过values.xml生成publicAttrsXml
+ ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName,
+ itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ File attrFile;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+ BufferedWriter outPublicAttrStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName, File attrFile) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ this.attrFile = attrFile;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+
+
+ if (attrFile != null) {
+ attrFile.getParentFile().mkdirs()
+ outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8"));
+ outPublicAttrStream.write("");
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.write("\n");
+ //唯独attr仍然使用010000开头,是为了和插件中的attr同组
+ outPublicAttrStream.write("\n \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n")
+ }
+
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+
+ if (outPublicAttrStream != null) {
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.flush();
+ outPublicAttrStream.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+
+ this.jarOutputStream.putNextEntry(newEntry);
+
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FairyPlugin/agp3_2_1/plugin.gradle b/FairyPlugin/agp3_2_1/plugin.gradle
new file mode 100644
index 00000000..587c0e74
--- /dev/null
+++ b/FairyPlugin/agp3_2_1/plugin.gradle
@@ -0,0 +1,801 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.zip.*
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.aaptOptions.additionalParameters(
+ '-I', currentSelectedBar,
+ '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用
+ '--allow-reserved-package-id')
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+
+ println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName
+
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ def hostVarDirName = props.getProperty("host.varDirName")
+ if (hostVarDirName == null) {
+ throw new Exception("HostInfo.prop文件信息不全,请检查此文件内容:" + propFile.absolutePath)
+ }
+ //应用宿主的主题包
+ applyThemePatch(hostVarDirName, varName, varDirName, hostBarExtraRootDir);
+ }
+
+ if (isApplicationModule) {
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ def hostVersionName = null
+ def hostVersionCode = null
+ def hostApplicationId = null
+ def hostBuildType = null
+ def hostFlavorName = null
+ def hostVarName = null
+ def hostVarDirName = null
+ if (propFile.exists()) {
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ hostVersionName = props.getProperty("host.versionName")
+ hostVersionCode = props.getProperty("host.versionCode")
+ hostBuildType = props.getProperty("host.buildType")
+ hostFlavorName = props.getProperty("host.flavorName")
+ hostVarName = props.getProperty("host.varName")
+ hostVarDirName = props.getProperty("host.varDirName")
+
+ hostApplicationId = props.getProperty("host.applicationId")
+
+ projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName)
+
+ } else {
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir)
+ } else {
+ throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath)
+ }
+ }
+
+ fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest:" + manifestFile.absolutePath
+ println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode
+ println "插入宿主ID hostApplicationId=" + hostApplicationId
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ println "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+ isFind = true;
+ def pluginObfuscatedJar = "plugin_obfuscated.jar"
+ def sourcePluginObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ //保留一个备份
+ project.logger.error "导出混淆后的插件jar包 "
+ println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar
+ println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar
+ copy {
+ from sourcePluginObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return pluginObfuscatedJar
+ }
+ })
+ }
+
+ diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar);
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+ }
+ }
+}
+
+def diffJar(String plugin, String host) {
+
+
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+
+ println "插件:" + plugin + ", 宿主:" + host
+
+ println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/"
+
+ ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/")
+ List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/")
+
+ println "再删掉插件的源 " + plugin
+ new File(plugin).delete()
+
+ println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName()
+ ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses)
+ println "重新生成的插件的源 " + plugin
+
+ //备份diff后的包
+ copy {
+ println "备份diff后重新生成的插件的源包到插件out目录"
+ println "From " + plugin
+ println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar"
+ from plugin
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return "plugin_obfuscated_after_diff.jar"
+ }
+ })
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
+
+def applyThemePatch(hostVarDirName, varName, varDir, hostPatchExtractDir) {
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + hostVarDirName;
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir"
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ file(resourceDir).mkdirs()
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '**/*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+ file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs()
+
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile")
+ "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor()
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
diff --git a/FairyPlugin/agp3_3_1/host.gradle b/FairyPlugin/agp3_3_1/host.gradle
new file mode 100644
index 00000000..050be0fc
--- /dev/null
+++ b/FairyPlugin/agp3_3_1/host.gradle
@@ -0,0 +1,1046 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.CRC32
+import java.util.zip.CheckedOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+
+configurations {
+ innerPlugin
+}
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+if ("false".equals(rootProject.properties.get("android.enableAapt2"))) {
+ throw new Exception("请开启aapt2编译开关:android.enableAapt2")
+}
+
+//generateSourcess时借此文件生成attrs.xml
+android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //收集需要内置的插件文件地址
+ configurations.innerPlugin.files.each { file ->
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ variant.getMergeResourcesProvider().configure {
+ it.doLast {
+
+ project.logger.error '生成一份主题patch包,编译非独立插件时需要此包'
+
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ patchDir.mkdirs();
+
+ //宿主资源id分组就靠这个文件了
+ def String publicAttrrsXml = "res/values/public_attrs.xml" ;
+ def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath;
+
+ //导出一份主题包备用,编译非独立插件时需要
+ createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile);
+
+ if(!file(publicAttrrsXmlFile).exists()) {
+ throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile)
+ }
+
+ String mergeResourcesOutPath = outputDir.absolutePath;
+
+ //需要确保aapt2在环境变量中,否则可能会执行失败
+ //Android Studio环境中的aapt2和命令行中的可能不是同一个版本,
+ //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2
+ //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2
+ //如果不是同一个版本可能出现Android Resource Link错误、magic number错误
+ //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ //把这个文件插入宿主的资源编译过程中是为了给宿主资源id分组,使得宿主资源id与将来的插件资源id不冲突
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile")
+ "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor()
+
+ //检查aapt2是否执行成功
+ if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) {
+ throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,影响宿主资源ID分组设置,请检查路径:$publicAttrrsXmlFile")
+ }
+ }
+ }
+
+ variant.getMergeAssetsProvider().configure {
+ it.setOnlyIf { true }
+ it.outputs.upToDateWhen { false }
+ it.doLast {
+
+ //检查内置插件坐标是否存在,不存在给出提示
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir
+ from files(innerPluginApkList)
+ into(outputDir)
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ //没有单独命名,有多个abi时文件会覆盖
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+ hostInfo.append("\nhost.varName=" + varName)
+ hostInfo.append("\nhost.varDirName=" + varDirName)
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Resources").doLast {
+ //修改此文件的内容,主要是设置用于id分组的id值,填充以及attr的值, 将来编译插件时需要此文件
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ def String publicAttrrsXmlFile = new File(patchDir.absolutePath, "res/values/public_attrs.xml").absolutePath;
+ project.logger.error "update public attrs ids, file=" + publicAttrrsXmlFile
+ String text = file(publicAttrrsXmlFile).text
+ .replace("public_static_final_host", "public_static_final_plugin")
+ .replace("0x7f3", "0x7f0")
+ .replace("0x7f4", "0x7f1")
+ file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el->
+ if (el.contains(":attr/")) {
+ String[] attrIdKeyValue = el.split("/")[1].split(" = ");
+ text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">")
+ }
+ }
+ file(publicAttrrsXmlFile).write(text)
+
+ //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件
+ //def processResFullName = baseVariant.apkData.fullName
+ copy {
+ project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+ println "from :" + resPackageOutputFolder
+ from resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+
+ //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用
+ variant.javaCompileProvider.configure {
+ it.doLast {
+
+ println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ classpath.each { jarPath ->
+ if (jarPath.isDirectory()) {
+ println "adding folder " + jarPath
+ jarMerger.addFolder(jarPath);
+ } else {
+ println "adding jar " + jarPath
+ jarMerger.addJar(jarPath);
+ }
+ }
+
+ File classes = new File(destinationDir.getParent(), "classes.jar");
+ classes.delete();
+
+ try {
+ ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList())
+ } catch(Exception e) {
+ throw new Exception("fail to create jar for app classes ")
+ }
+
+ if (classes.exists()) {
+ println "adding jar " + classes
+ jarMerger.addJar(classes)
+ } else {
+ throw new Exception("Not exists : classes file path is " + classes.absolutePath)
+ }
+
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+ }
+
+ if (variant.buildType["minifyEnabled"]) {
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${varName.capitalize()}")
+ proguardTask.doFirst {
+ println "开始混淆任务:" + varName.capitalize()
+ }
+ proguardTask.doLast {
+ println "混淆完成:" + varName.capitalize()
+ boolean isFind = false;
+ proguardTask.outputs.files.files.each { File file->
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ project.logger.error "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+
+ isFind = true;
+ def sourceHostObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ def hostObfuscatedJar = "host_obfuscated.jar"
+ project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar
+
+ copy {
+ from sourceHostObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ project.logger.error "rename:" + s
+ return hostObfuscatedJar
+ }
+ })
+ }
+ }
+ }
+ if (!isFind) {
+ throw new Exception("obfuscated jar file not found, please check.")
+ }
+ }
+ }
+
+
+ //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+ task "makeHostBaseLine${varName.capitalize()}"(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ version android.defaultConfig.versionName
+ classifier "${varName.capitalize()}"
+ from zipTree("${project.buildDir}/outputs/resources.ap_")
+ from("${project.buildDir}/outputs") {
+ exclude '*.ap_'
+ }
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}"
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//导出主题patch
+def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) {
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/"
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml',
+ 'values-v1*/values-v1*.xml',
+ 'values-v2*/values-v2*.xml',
+ 'values-*-v1*/values-*-v1*.xml',
+ 'values-*-v4/values-*-v4.xml',
+ 'values-land/values-land.xml',
+ 'values-*-v2*/values-*-v2*.xml',
+ 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "")
+ def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name)
+ destFile.getParentFile().mkdirs()
+ println "export from " + itemFile + " to " + destFile
+
+ //通过values.xml生成publicAttrsXml
+ ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName,
+ itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ File attrFile;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+ BufferedWriter outPublicAttrStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName, File attrFile) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ this.attrFile = attrFile;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+
+
+ if (attrFile != null) {
+ attrFile.getParentFile().mkdirs()
+ outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8"));
+ outPublicAttrStream.write("");
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.write("\n");
+ //唯独attr仍然使用010000开头,是为了和插件中的attr同组
+ outPublicAttrStream.write("\n \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n")
+ }
+
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ if (tag.contains("type=\"reference\"")) {
+ tag = tag.replace("type=\"reference\"", " ");
+ }
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+
+ if (outPublicAttrStream != null) {
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.flush();
+ outPublicAttrStream.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+
+ this.jarOutputStream.putNextEntry(newEntry);
+
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FairyPlugin/agp3_3_1/plugin.gradle b/FairyPlugin/agp3_3_1/plugin.gradle
new file mode 100644
index 00000000..e3ae8bb6
--- /dev/null
+++ b/FairyPlugin/agp3_3_1/plugin.gradle
@@ -0,0 +1,811 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.zip.*
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.aaptOptions.additionalParameters(
+ '-I', currentSelectedBar,
+ '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用
+ '--allow-reserved-package-id')
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+
+ println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName
+
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ def hostVarDirName = props.getProperty("host.varDirName")
+ if (hostVarDirName == null) {
+ throw new Exception("HostInfo.prop文件信息不全,请检查此文件内容:" + propFile.absolutePath)
+ }
+ //应用宿主的主题包
+ applyThemePatch(hostVarDirName, varName, varDirName, hostBarExtraRootDir);
+ }
+
+ if (isApplicationModule) {
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+ tasks.findByName("compile${variant.name.capitalize()}Sources").doLast {
+ project.copy {
+ from(buildDir.absolutePath + '/intermediates/app_classes/' + varName +'/bundle' + varName.capitalize() + 'Classes') {
+ include "classes.jar"
+ }
+ into(buildDir.absolutePath + "/outputs")
+ rename('classes', project.name + "-" + varName)
+ }
+ }
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ def hostVersionName = null
+ def hostVersionCode = null
+ def hostApplicationId = null
+ def hostBuildType = null
+ def hostFlavorName = null
+ def hostVarName = null
+ def hostVarDirName = null
+ if (propFile.exists()) {
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ hostVersionName = props.getProperty("host.versionName")
+ hostVersionCode = props.getProperty("host.versionCode")
+ hostBuildType = props.getProperty("host.buildType")
+ hostFlavorName = props.getProperty("host.flavorName")
+ hostVarName = props.getProperty("host.varName")
+ hostVarDirName = props.getProperty("host.varDirName")
+
+ hostApplicationId = props.getProperty("host.applicationId")
+
+ projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName)
+
+ } else {
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir)
+ } else {
+ throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath)
+ }
+ }
+
+ fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest:" + manifestFile.absolutePath
+ println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode
+ println "插入宿主ID hostApplicationId=" + hostApplicationId
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ println "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) {
+ isFind = true;
+ def pluginObfuscatedJar = "plugin_obfuscated.jar"
+ def sourcePluginObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ }
+ //保留一个备份
+ project.logger.error "导出混淆后的插件jar包 "
+ println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar
+ println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar
+ copy {
+ from sourcePluginObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return pluginObfuscatedJar
+ }
+ })
+ }
+
+ diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar);
+ }
+ }
+ if (!isFind) {
+ throw "obfuscated jar file not found, please check."
+ }
+ }
+ }
+ }
+ }
+}
+
+def diffJar(String plugin, String host) {
+
+
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+
+ println "插件:" + plugin + ", 宿主:" + host
+
+ println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/"
+
+ ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/")
+ List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/")
+
+ println "再删掉插件的源 " + plugin
+ new File(plugin).delete()
+
+ println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName()
+ ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses)
+ println "重新生成的插件的源 " + plugin
+
+ //备份diff后的包
+ copy {
+ println "备份diff后重新生成的插件的源包到插件out目录"
+ println "From " + plugin
+ println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar"
+ from plugin
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return "plugin_obfuscated_after_diff.jar"
+ }
+ })
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
+
+def applyThemePatch(hostVarDirName, varName, varDir, hostPatchExtractDir) {
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + hostVarDirName;
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir"
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ file(resourceDir).mkdirs()
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '**/*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+ file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs()
+
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile")
+ "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor()
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
diff --git a/FairyPlugin/agp3_3_2/host.gradle b/FairyPlugin/agp3_3_2/host.gradle
new file mode 100644
index 00000000..a8200819
--- /dev/null
+++ b/FairyPlugin/agp3_3_2/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_3_2/plugin.gradle b/FairyPlugin/agp3_3_2/plugin.gradle
new file mode 100644
index 00000000..8b2632df
--- /dev/null
+++ b/FairyPlugin/agp3_3_2/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_0/host.gradle b/FairyPlugin/agp3_4_0/host.gradle
new file mode 100644
index 00000000..a8200819
--- /dev/null
+++ b/FairyPlugin/agp3_4_0/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_0/plugin.gradle b/FairyPlugin/agp3_4_0/plugin.gradle
new file mode 100644
index 00000000..8b2632df
--- /dev/null
+++ b/FairyPlugin/agp3_4_0/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_1/host.gradle b/FairyPlugin/agp3_4_1/host.gradle
new file mode 100644
index 00000000..a8200819
--- /dev/null
+++ b/FairyPlugin/agp3_4_1/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_1/plugin.gradle b/FairyPlugin/agp3_4_1/plugin.gradle
new file mode 100644
index 00000000..8b2632df
--- /dev/null
+++ b/FairyPlugin/agp3_4_1/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_2/host.gradle b/FairyPlugin/agp3_4_2/host.gradle
new file mode 100644
index 00000000..a8200819
--- /dev/null
+++ b/FairyPlugin/agp3_4_2/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_4_2/plugin.gradle b/FairyPlugin/agp3_4_2/plugin.gradle
new file mode 100644
index 00000000..8b2632df
--- /dev/null
+++ b/FairyPlugin/agp3_4_2/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_5_0/host.gradle b/FairyPlugin/agp3_5_0/host.gradle
new file mode 100644
index 00000000..a8200819
--- /dev/null
+++ b/FairyPlugin/agp3_5_0/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_5_0/plugin.gradle b/FairyPlugin/agp3_5_0/plugin.gradle
new file mode 100644
index 00000000..8b2632df
--- /dev/null
+++ b/FairyPlugin/agp3_5_0/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp3_5_3/host.gradle b/FairyPlugin/agp3_5_3/host.gradle
new file mode 100644
index 00000000..baccaab9
--- /dev/null
+++ b/FairyPlugin/agp3_5_3/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle"
diff --git a/FairyPlugin/agp3_5_3/plugin.gradle b/FairyPlugin/agp3_5_3/plugin.gradle
new file mode 100644
index 00000000..8e5a4dda
--- /dev/null
+++ b/FairyPlugin/agp3_5_3/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle"
diff --git a/FairyPlugin/agp4_1_3/host.gradle b/FairyPlugin/agp4_1_3/host.gradle
new file mode 100644
index 00000000..45f0bc2a
--- /dev/null
+++ b/FairyPlugin/agp4_1_3/host.gradle
@@ -0,0 +1,1062 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.CRC32
+import java.util.zip.CheckedOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+
+configurations {
+ innerPlugin
+}
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+if ("false".equals(rootProject.properties.get("android.enableAapt2"))) {
+ throw new Exception("请开启aapt2编译开关:android.enableAapt2")
+}
+
+//generateSourcess时借此文件生成attrs.xml
+android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //收集需要内置的插件文件地址
+ configurations.innerPlugin.files.each { file ->
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ variant.getMergeResourcesProvider().configure {
+ it.doLast {
+
+ project.logger.error '生成一份主题patch包,编译非独立插件时需要此包'
+
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ patchDir.mkdirs();
+
+ //宿主资源id分组就靠这个文件了
+ def String publicAttrrsXml = "res/values/public_attrs.xml" ;
+ def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath;
+
+ //导出一份主题包备用,编译非独立插件时需要
+ createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile);
+
+ if(!file(publicAttrrsXmlFile).exists()) {
+ throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile)
+ }
+
+ String mergeResourcesOutPath = outputDir.orNull;
+
+ //需要确保aapt2在环境变量中,否则可能会执行失败
+ //Android Studio环境中的aapt2和命令行中的可能不是同一个版本,
+ //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2
+ //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2
+ //如果不是同一个版本可能出现Android Resource Link错误、magic number错误
+ //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ //把这个文件插入宿主的资源编译过程中是为了给宿主资源id分组,使得宿主资源id与将来的插件资源id不冲突
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile")
+ "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor()
+
+ //检查aapt2是否执行成功
+ if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) {
+ throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,影响宿主资源ID分组设置,请检查路径:$publicAttrrsXmlFile")
+ }
+ }
+ }
+
+ variant.getMergeAssetsProvider().configure {
+ it.setOnlyIf { true }
+ it.outputs.upToDateWhen { false }
+ it.doLast {
+
+ //检查内置插件坐标是否存在,不存在给出提示
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull
+ from files(innerPluginApkList)
+ into(outputDir)
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ //没有单独命名,有多个abi时文件会覆盖
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+ hostInfo.append("\nhost.varName=" + varName)
+ hostInfo.append("\nhost.varDirName=" + varDirName)
+ hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled)
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Resources").doLast {
+ //修改此文件的内容,主要是设置用于id分组的id值,填充以及attr的值, 将来编译插件时需要此文件
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ def String publicAttrrsXmlFile = new File(patchDir.absolutePath, "res/values/public_attrs.xml").absolutePath;
+ project.logger.error "update public attrs ids, file=" + publicAttrrsXmlFile
+ String text = file(publicAttrrsXmlFile).text
+ .replace("public_static_final_host", "public_static_final_plugin")
+ .replace("0x7f3", "0x7f0")
+ .replace("0x7f4", "0x7f1")
+ file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el->
+ if (el.contains(":attr/")) {
+ String[] attrIdKeyValue = el.split("/")[1].split(" = ");
+ text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">")
+ }
+ }
+ file(publicAttrrsXmlFile).write(text)
+
+ //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件
+ //def processResFullName = baseVariant.apkData.fullName
+ copy {
+ project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+ println "from :" + resPackageOutputFolder
+ from resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+
+ //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用
+ variant.javaCompileProvider.configure {
+ it.doLast {
+
+ println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ classpath.each { jarPath ->
+ if (jarPath.isDirectory()) {
+ println "adding folder " + jarPath
+ jarMerger.addFolder(jarPath);
+ } else {
+ println "adding jar " + jarPath
+ jarMerger.addJar(jarPath);
+ }
+ }
+
+ File classes = new File(destinationDir.getParent(), "classes.jar");
+ classes.delete();
+
+ try {
+ ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList())
+ } catch(Exception e) {
+ throw new Exception("fail to create jar for app classes ")
+ }
+
+ if (classes.exists()) {
+ println "adding jar " + classes
+ jarMerger.addJar(classes)
+ } else {
+ throw new Exception("Not exists : classes file path is " + classes.absolutePath)
+ }
+
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+ }
+
+ if (variant.buildType.minifyEnabled) {
+ if (!"false".equals(rootProject.properties.get("android.enableR8"))) {
+ throw new Exception("请关闭R8:android.enableR8=false")
+ }
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${varName.capitalize()}")
+ if (proguardTask == null) {
+ proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithProguard")
+ }
+ if (proguardTask != null) {
+ proguardTask.doFirst {
+ println "开始混淆任务:" + varName.capitalize()
+ }
+ proguardTask.doLast {
+ println "混淆完成:" + varName.capitalize()
+ boolean isFind = false;
+ proguardTask.outputs.files.files.each { File file->
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ project.logger.error "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if ((file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName))
+ || file.absolutePath.contains("/shrunk_jar/")) {
+
+ isFind = true;
+ def sourceHostObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ } else if (file.absolutePath.endsWith("/minified.jar")) {
+ sourceHostObfuscatedJar = file.absolutePath
+ }
+ def hostObfuscatedJar = "host_obfuscated.jar"
+ project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar
+
+ copy {
+ from sourceHostObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ project.logger.error "rename:" + s
+ return hostObfuscatedJar
+ }
+ })
+ }
+ }
+ }
+ if (!isFind) {
+ throw new Exception("obfuscated jar file not found, please check.")
+ }
+ }
+ }
+ }
+
+
+ //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+ task "makeHostBaseLine${varName.capitalize()}"(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ version android.defaultConfig.versionName
+ classifier "${varName.capitalize()}"
+ from zipTree("${project.buildDir}/outputs/resources.ap_")
+ from("${project.buildDir}/outputs") {
+ exclude '*.ap_'
+ }
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}"
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//导出主题patch
+def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) {
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/"
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml',
+ 'values-v1*/values-v1*.xml',
+ 'values-v2*/values-v2*.xml',
+ 'values-*-v1*/values-*-v1*.xml',
+ 'values-*-v4/values-*-v4.xml',
+ 'values-land/values-land.xml',
+ 'values-*-v2*/values-*-v2*.xml',
+ 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "")
+ def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name)
+ destFile.getParentFile().mkdirs()
+ println "export from " + itemFile + " to " + destFile
+
+ //通过values.xml生成publicAttrsXml
+ ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName,
+ itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ File attrFile;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+ BufferedWriter outPublicAttrStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName, File attrFile) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ this.attrFile = attrFile;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+
+
+ if (attrFile != null) {
+ attrFile.getParentFile().mkdirs()
+ outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8"));
+ outPublicAttrStream.write("");
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.write("\n");
+ //唯独attr仍然使用010000开头,是为了和插件中的attr同组
+ outPublicAttrStream.write("\n \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ " \n" +
+ " \n")
+ }
+
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ if (tag.contains("type=\"reference\"")) {
+ tag = tag.replace("type=\"reference\"", " ");
+ }
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+
+ if (outPublicAttrStream != null) {
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.flush();
+ outPublicAttrStream.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+ try {
+ this.jarOutputStream.putNextEntry(newEntry);
+ } catch(Exception duplicate){
+ //类重复了,先忽略吧
+ println "addJar putNextEntry fail: " + name
+ continue;
+ }
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/FairyPlugin/agp4_1_3/plugin.gradle b/FairyPlugin/agp4_1_3/plugin.gradle
new file mode 100644
index 00000000..eaf28d9b
--- /dev/null
+++ b/FairyPlugin/agp4_1_3/plugin.gradle
@@ -0,0 +1,806 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.zip.*
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.aaptOptions.additionalParameters(
+ '-I', currentSelectedBar,
+ '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用
+ '--allow-reserved-package-id')
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ //读取宿主编译信息
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ props.keySet().forEach { propKey->
+ project.ext.setProperty(propKey, props.getProperty(propKey))
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ if (!"false".equals(rootProject.properties.get("android.enableR8"))) {
+ throw new Exception("请关闭R8:android.enableR8=false")
+ }
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+ //应用宿主的主题包
+ applyThemePatch(varName, varDirName, hostBarExtraRootDir);
+ }
+
+ if (isApplicationModule) {
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+ tasks.findByName("compile${variant.name.capitalize()}Sources").doLast {
+ project.copy {
+ from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) {
+ include "classes.jar"
+ }
+ into(buildDir.absolutePath + "/outputs")
+ rename('classes', project.name + "-" + varName)
+ }
+ }
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ def hostVersionName = project.ext.getProperty("host.versionName")
+ def hostVersionCode = project.ext.getProperty("host.versionCode")
+ def hostApplicationId = project.ext.getProperty("host.applicationId")
+ def hostBuildType = project.ext.getProperty("host.buildType")
+ def hostFlavorName = project.ext.getProperty("host.flavorName")
+ def hostVarName = project.ext.getProperty("host.varName")
+ def hostVarDirName = project.ext.getProperty("host.varDirName")
+ if (hostApplicationId != null) {
+ projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName)
+ } else {
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir)
+ } else {
+ throw new Exception("宿主基线包信息丢失")
+ }
+ }
+
+ fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest:" + manifestFile.absolutePath
+ println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode
+ println "插入宿主ID hostApplicationId=" + hostApplicationId
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ //http://blog.csdn.net/sbsujjbcy/article/details/50839263
+ //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar
+ //最新版本路径已发生变化;2017-11-12
+ println "file outputs=>${file.absolutePath}"
+ String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator;
+ println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName))
+ if ((file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName))
+ || file.absolutePath.contains("/shrunk_jar/")) {
+ isFind = true;
+ def pluginObfuscatedJar = "plugin_obfuscated.jar"
+ def sourcePluginObfuscatedJar
+ if (new File(file.absolutePath + "/0.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/0.jar"
+ } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) {
+ sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar"
+ } else if (file.absolutePath.endsWith("/minified.jar")) {
+ sourcePluginObfuscatedJar = file.absolutePath
+ }
+ //保留一个备份
+ project.logger.error "导出混淆后的插件jar包 "
+ println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录
+ println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar
+ copy {
+ from sourcePluginObfuscatedJar
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return pluginObfuscatedJar
+ }
+ })
+ }
+
+ diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar);
+ }
+ }
+ if (!isFind) {
+ throw new Exception("obfuscated jar file not found, please check.")
+ }
+ }
+ }
+ }
+ }
+}
+
+def diffJar(String plugin, String host) {
+
+
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+
+ println "插件:" + plugin + ", 宿主:" + host
+
+ println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/"
+
+ ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/")
+ List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/")
+
+ println "再删掉插件的源 " + plugin
+ new File(plugin).delete()
+
+ println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + ", " + file(plugin).getName()
+ ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), file(plugin).getName(), hostClasses)
+ println "重新生成的插件的源 " + plugin
+
+ //备份diff后的包
+ copy {
+ println "备份diff后重新生成的插件的源包到插件out目录"
+ println "From " + plugin
+ println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar"
+ from plugin
+ into("${project.buildDir}/outputs/")
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ return "plugin_obfuscated_after_diff.jar"
+ }
+ })
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
+
+def applyThemePatch(varName, varDir, hostPatchExtractDir) {
+ println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir
+
+ if (!file(hostPatchExtractDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir);
+ }
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName")
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir"
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ file(resourceDir).mkdirs()
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '**/*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+ file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs()
+
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile")
+ "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor()
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
diff --git a/FairyPlugin/agp4_2_1/host.gradle b/FairyPlugin/agp4_2_1/host.gradle
new file mode 100644
index 00000000..32f59967
--- /dev/null
+++ b/FairyPlugin/agp4_2_1/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/host.gradle"
diff --git a/FairyPlugin/agp4_2_1/plugin.gradle b/FairyPlugin/agp4_2_1/plugin.gradle
new file mode 100644
index 00000000..452b73d8
--- /dev/null
+++ b/FairyPlugin/agp4_2_1/plugin.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/plugin.gradle"
diff --git a/FairyPlugin/agp7_0_3/host.gradle b/FairyPlugin/agp7_0_3/host.gradle
new file mode 100644
index 00000000..47d7f1cc
--- /dev/null
+++ b/FairyPlugin/agp7_0_3/host.gradle
@@ -0,0 +1,953 @@
+import org.xml.sax.Attributes
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.regex.Pattern
+import java.util.zip.CRC32
+import java.util.zip.CheckedOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+
+configurations {
+ innerPlugin
+}
+
+def innerPluginApkList = []
+
+class FaiyExt {
+ private String pluginProcess = ":plugin"
+
+ //指定当前宿主版本与哪些历史宿主版本兼容
+ private String compatibleWithHostVersion = null
+
+ public String getPluginProcess() {
+ return pluginProcess
+ }
+
+ public void setPluginProcess(String process) {
+ this.pluginProcess = process
+ }
+
+ public String getCompatibleWithHostVersion() {
+ return compatibleWithHostVersion
+ }
+
+ public void setCompatibleWithHostVersion(String compatibleVersion) {
+ this.compatibleWithHostVersion = compatibleVersion
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+if ("false".equals(rootProject.properties.get("android.enableAapt2"))) {
+ throw new Exception("请开启aapt2编译开关:android.enableAapt2")
+}
+
+//generateSourcess时借此文件生成attrs.xml
+android.androidResources.additionalParameters(
+ '--emit-ids', project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties",
+ '--stable-ids', project.buildDir.absolutePath + "/outputs/public_ids.properties")
+
+afterEvaluate {
+
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //收集需要内置的插件文件地址
+ configurations.innerPlugin.files.each { file ->
+ innerPluginApkList << file.absolutePath
+ }
+
+ for (variant in android.applicationVariants) {
+
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+
+ println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ variant.getMergeResourcesProvider().configure {
+ it.doLast {
+ project.logger.error '生成一份主题patch包,编译非独立插件时需要此包'
+
+ File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName);
+ patchDir.mkdirs();
+
+ //导出一份主题包备用,编译非独立插件时需要
+ createThemePatch(varName, buildTypeName, patchDir);
+ }
+ }
+
+ variant.getMergeAssetsProvider().configure {
+ it.setOnlyIf { true }
+ it.outputs.upToDateWhen { false }
+ it.doLast {
+
+ //检查内置插件坐标是否存在,不存在给出提示
+ innerPluginApkList.each { innerAPK ->
+ if (!file(innerAPK).exists()) {
+ project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK
+ }
+ }
+
+ copy {
+ project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull
+ from files(innerPluginApkList)
+ into(outputDir)
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath
+
+ def originManifestContent = manifestFile.getText('UTF-8')
+ if (originManifestContent.contains("{applicationId}")) {
+ throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId")
+ }
+
+ //生成prop文件
+ def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\"");
+ def matcher = pattern.matcher(originManifestContent);
+ if (matcher.find()) {
+ def versionName = matcher.group(1)
+ //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop")
+ println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!'
+ File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop")
+ if (hostInfo.exists()) {
+ hostInfo.delete()
+ }
+ //没有单独命名,有多个abi时文件会覆盖
+ println '正在生成文件' + hostInfo.absolutePath
+ hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss"))
+ hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode)
+ //versionName可能有后缀,所以以Manifest中为准
+ hostInfo.append("\nhost.versionName=" + versionName)
+ hostInfo.append("\nhost.buildType=" + buildTypeName)
+ hostInfo.append("\nhost.flavorName=" + flavorName)
+ hostInfo.append("\nhost.varName=" + varName)
+ hostInfo.append("\nhost.varDirName=" + varDirName)
+ hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled)
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildTypeName].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix;
+ }
+
+ hostInfo.append("\nhost.applicationId=" + packageName)
+ }
+
+ //指定插件进程名,设置为空串或者null即是和宿主同进程
+ //不设置即使用默认进程(:plugin)
+ def pluginProcessName = fairy.pluginProcess
+ if (!":plugin".equals(pluginProcessName)) {
+ def customPluginProcessName = "";
+ if (pluginProcessName != null) {
+ customPluginProcessName = "android:process=\"" + pluginProcessName + "\""
+ }
+ println '正在设置插件进程配置:' + customPluginProcessName
+ def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName)
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+
+ //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本
+ //例如:
+ // 宿主版本v1,插件版本v1.1, v1.2
+ // 宿主版本v2,插件版本v2.1, v2.1
+ // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行
+ //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行
+ //此功能请谨慎使用,否则可能引起插件崩溃
+ def compatibleWithHostVersion = fairy.compatibleWithHostVersion
+ if(compatibleWithHostVersion != null) {
+ originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim())
+ manifestFile.write(modifyedManifestContent, 'UTF-8')
+ }
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Resources").doFirst {
+ File public_ids = file(project.buildDir.absolutePath + "/outputs/public_ids.properties")
+ if (!public_ids.exists()) {
+ def packageName = android.defaultConfig.applicationId
+ public_ids.write(
+ packageName + ":attr/public_static_final_host_attr_ = 0x7f010000\n" +
+ packageName + ":drawable/public_static_final_host_drawable_ = 0x7f320000\n" +
+ packageName + ":layout/public_static_final_host_layout_ = 0x7f330000\n" +
+ packageName + ":anim/public_static_final_host_anim_ = 0x7f340000\n" +
+ packageName + ":xml/public_static_final_host_xml_ = 0x7f350000\n" +
+ packageName + ":raw/public_static_final_host_raw_ = 0x7f360000\n" +
+ packageName + ":dimen/public_static_final_host_dimen_ = 0x7f370000\n" +
+ packageName + ":string/public_static_final_host_string_ = 0x7f380000\n" +
+ packageName + ":style/public_static_final_host_style_ = 0x7f390000\n" +
+ packageName + ":color/public_static_final_host_color_ = 0x7f3a0000\n" +
+ packageName + ":id/public_static_final_host_id_ = 0x7f3b0000\n" +
+ packageName + ":bool/public_static_final_host_bool_ = 0x7f3c0000\n" +
+ packageName + ":integer/public_static_final_host_int_ = 0x7f3d0000\n" +
+ packageName + ":array/public_static_final_host_array_ = 0x7f3e0000\n" +
+ packageName + ":menu/public_static_final_host_menu_ = 0x7f3f0000\n" +
+ packageName + ":mipmap/public_static_final_host_mipmap_ = 0x7f400000\n" +
+ packageName + ":animator/public_static_final_host_animator_ = 0x7f410000\n" +
+ packageName + ":fraction/public_static_final_host_fraction_ = 0x7f420000\n" +
+ packageName + ":font/public_static_final_host_font_ = 0x7f430000\n" +
+ packageName + ":plurals/public_static_final_host_plurals_ = 0x7f440000\n" +
+ packageName + ":interpolator/public_static_final_host_interpolator_ = 0x7f450000\n" +
+ packageName + ":transition/public_static_final_host_transition_ = 0x7f460000\n")
+ }
+ }
+
+ tasks.findByName("process${varName.capitalize()}Resources").doLast {
+ copy {
+ from buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties"
+ into buildDir.absolutePath + "/outputs/"
+ rename { "public_attrs.properties" }
+ filter { String line ->
+ (line.contains(":attr/") || line.contains("public_static_final_host")) ? line : null
+ }
+ }
+
+ //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件
+ //def processResFullName = baseVariant.apkData.fullName
+ copy {
+ project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包'
+ println "from :" + resPackageOutputFolder
+ from resPackageOutputFolder
+ include("*.ap_")
+ into("${project.buildDir}/outputs/")
+ duplicatesStrategy = DuplicatesStrategy.WARN
+ rename(new Transformer() {
+ @Override
+ String transform(String s) {
+ //多abi时会相互覆盖,不过对我们而言应该没什么影响
+ project.logger.error "rename: " + s
+ return "resources.ap_"
+ }
+ })
+ }
+ }
+
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+
+ //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用
+ variant.javaCompileProvider.configure {
+ it.doLast {
+ println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName
+ it.inputs.files.each { inputsFile->
+ //println "inputs: " + inputsFile
+ }
+
+ File jarFile = new File(project.buildDir, "outputs/host_classes.jar")
+ if (jarFile.exists()) {
+ jarFile.delete()
+ }
+
+ JarMerger jarMerger = new JarMerger(jarFile)
+ try {
+ jarMerger.setFilter(new JarFilter() {
+ public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException {
+ if (archivePath.endsWith(".class")) {
+ return true
+ }
+ return false
+ }
+ });
+
+ classpath.each { jarPath ->
+ if (jarPath.isDirectory()) {
+ println "adding folder " + jarPath
+ jarMerger.addFolder(jarPath);
+ } else {
+ println "adding jar " + jarPath
+ jarMerger.addJar(jarPath);
+ }
+ }
+
+ File classes = new File(destinationDir.getParent(), "classes.jar");
+ classes.delete();
+
+ try {
+ ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList())
+ } catch(Exception e) {
+ throw new Exception("fail to create jar for app classes ")
+ }
+
+ if (classes.exists()) {
+ println "adding jar " + classes
+ jarMerger.addJar(classes)
+ } else {
+ throw new Exception("Not exists : classes file path is " + classes.absolutePath)
+ }
+
+ } finally {
+ jarMerger.close()
+ }
+
+ println "Merge Jar Finished, Jar is at " + jarFile.absolutePath
+ }
+ }
+
+ if (variant.buildType.minifyEnabled) {
+ def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8")
+ if (proguardTask != null) {
+ proguardTask.doLast {
+ proguardTask.outputs.files.files.each { File file->
+ if (file.absolutePath.endsWith(proguardTask.name)) {
+ println "file outputs=>${file.absolutePath}"
+ copy {
+ from file.absolutePath
+ into("${project.buildDir}/outputs/minifyWithR8")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译
+ task "makeHostBaseLine${varName.capitalize()}"(type: Zip) {
+ extension "bar" //Baseline Application Resource
+ baseName 'host'
+ version android.defaultConfig.versionName
+ classifier "${varName.capitalize()}"
+ from zipTree("${project.buildDir}/outputs/resources.ap_")
+ from("${project.buildDir}/outputs") {
+ exclude '*.ap_'
+ }
+ }
+
+ //导出宿主最终的基线包
+ tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}"
+ }
+
+ if (gradle.startParameter.taskNames.find {
+ println ">>>>>>执行命令: " + it
+ it.startsWith("assemble") || it.startsWith("build")
+ } != null) {
+ //nothing
+ }
+}
+
+//导出主题patch
+def createThemePatch(String varName, String buildType, File patchDir) {
+
+ def packageName = android.defaultConfig.applicationId
+ if (android.buildTypes[buildType].applicationIdSuffix != null) {
+ packageName = packageName + android.buildTypes[buildType].applicationIdSuffix;
+ }
+
+ String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/"
+ FileTree allxmlFiles = fileTree(dir: mergedResDir)
+ allxmlFiles.include 'values/values*.xml',
+ 'values-v1*/values-v1*.xml',
+ 'values-v2*/values-v2*.xml',
+ 'values-*-v1*/values-*-v1*.xml',
+ 'values-*-v4/values-*-v4.xml',
+ 'values-land/values-land.xml',
+ 'values-*-v2*/values-*-v2*.xml',
+ 'values-*-v8/values-*-v8.xml'
+
+ allxmlFiles.each { File itemFile ->
+ def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "")
+ def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name)
+ destFile.getParentFile().mkdirs()
+ println "export from " + itemFile + " to " + destFile
+ ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName)
+ }
+}
+
+public class ThemeProcessor extends DefaultHandler {
+
+ public static void exportThemeStyle(File srcFile, File destFile, String packageName) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName));
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ File destFile;
+ String packageName;
+ Stack stack = new Stack();
+ BufferedWriter outXmlStream = null;
+ BufferedWriter outPublicAttrStream = null;
+
+ HashSet attrSets = new HashSet<>();
+
+ HashSet dupcate = new HashSet<>();
+
+ public ThemeProcessor(File destFile, String packageName) {
+ this.destFile = destFile;
+ this.packageName = packageName;
+ }
+
+ public void startDocument() throws SAXException {
+ try {
+ outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"));
+ outXmlStream.write("");
+ outXmlStream.write("\n");
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+
+ if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment")
+ || qName.equals("integer") || qName.equals("string")
+ || qName.equals("ns2:g") || qName.equals("ns1:g")
+ || qName.equals("u")) {
+ stack.add(new Node(attributes.getValue("name"), false, true));
+ return;
+ }
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+ try {
+ if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true)))
+ || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) {
+ //skip
+ skip = true;
+ } else {
+ if (qName.equals("enum")) {
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ if (!dupcate.contains(top.name)) {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ } else {
+ outXmlStream.write(tag);
+ if (qName.equals("attr") && outPublicAttrStream != null) {
+ outPublicAttrStream.write(tag.replace("")
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ if (!stack.empty()) {
+ Node parent = stack.peek();
+ if (qName.equals("enum") && dupcate.contains(parent.name)) {
+ //nothing
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } else {
+ outXmlStream.write(space + "" + qName + ">");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ String text = new String(ch, start, length);
+ text = text.replaceAll("[\n ]", "");
+ if (text.length() > 0) {
+ try {
+ if (text.startsWith("@color")) {
+ text = text.replace("@color", "@*" + packageName +":color");
+
+ } else if (text.startsWith("@dimen")) {
+ text = text.replace("@dimen", "@*" + packageName +":dimen");
+
+ } else if (text.startsWith("@string")) {
+ text = text.replace("@string", "@*" + packageName +":string");
+
+ } else if (text.startsWith("@bool")) {
+ text = text.replace("@bool", "@*" + packageName +":bool");
+
+ } else if (text.startsWith("@integer")) {
+ text = text.replace("@integer", "@*" + packageName +":integer");
+
+ } else if (text.startsWith("@layout")) {
+ text = text.replace("@layout", "@*" + packageName +":layout");
+
+ } else if (text.startsWith("@anim")) {
+ text = text.replace("@anim", "@*" + packageName +":anim");
+
+ } else if (text.startsWith("@id")) {
+ text = text.replace("@id", "@*" + packageName +":id");
+
+ } else if (text.startsWith("@drawable")) {
+ text = text.replace("@drawable", "@*" + packageName +":drawable");
+
+ //} else if (text.startsWith("?attr")) {
+ // text = text.replace("?attr", "?*" + packageName +":attr");
+ } else if (text.startsWith("@mipmap")) {
+ text = text.replace("@mipmap", "@*" + packageName +":mipmap");
+ } else if (text.startsWith("@style")) {
+ if (node.name.equals("android:windowAnimationStyle")) {
+ text = text.replace("@style", "@*" + packageName +":style");
+ }
+ }
+
+ outXmlStream.write(text);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlStream.flush();
+ outXmlStream.close();
+
+ if (outPublicAttrStream != null) {
+ outPublicAttrStream.write("\n");
+ outPublicAttrStream.flush();
+ outPublicAttrStream.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+}
+
+public class JarMerger {
+ private final byte[] buffer = new byte[8192];
+ private final File jarFile;
+ private FileOutputStream fos;
+ private JarOutputStream jarOutputStream;
+
+ private JarFilter filter;
+
+ public JarMerger(File jarFile) throws IOException {
+ this.jarFile = jarFile;
+ }
+
+ private void init() throws IOException {
+ if(this.fos == null && this.jarOutputStream == null) {
+ if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) {
+ throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile());
+ }
+ this.fos = new FileOutputStream(this.jarFile);
+ this.jarOutputStream = new JarOutputStream(fos);
+ }
+ }
+
+ public void setFilter(JarFilter filter) {
+ this.filter = filter;
+ }
+
+ public void addFolder(File folder) throws IOException {
+ this.init();
+
+ try {
+ this.addFolderInternal(folder, "");
+ } catch (JarFilter.ZipAbortException var3) {
+ throw new IOException(var3);
+ }
+ }
+
+ private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException {
+ File[] files = folder.listFiles();
+ if(files != null) {
+ File[] arr$ = files;
+ int len$ = files.length;
+
+ for(int i$ = 0; i$ < len$; ++i$) {
+ File file = arr$[i$];
+ if(!file.isFile()) {
+ if(file.isDirectory()) {
+ this.addFolderInternal(file, path + file.getName() + "/");
+ }
+ } else {
+ String entryPath = path + file.getName();
+ if(this.filter == null || this.filter.checkEntry(entryPath)) {
+ this.jarOutputStream.putNextEntry(new JarEntry(entryPath));
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+
+ int count;
+ while((count = fis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+ } finally {
+ if (fis != null) {
+ fis.close();
+ fis = null;
+ }
+ }
+
+ this.jarOutputStream.closeEntry();
+ }
+ }
+ }
+ }
+
+ }
+
+ public void addJar(File file) throws IOException {
+ this.addJar(file, false);
+ }
+
+ public void addJar(File file, boolean removeEntryTimestamp) throws IOException {
+ this.init();
+
+ FileInputStream e = null;
+ ZipInputStream zis = null;
+ try {
+ e = new FileInputStream(file);
+ zis = new ZipInputStream(e);
+
+ ZipEntry entry;
+ while((entry = zis.getNextEntry()) != null) {
+ if(!entry.isDirectory()) {
+ String name = entry.getName();
+ if(this.filter == null || this.filter.checkEntry(name)) {
+ JarEntry newEntry;
+ if(entry.getMethod() == ZipEntry.STORED) {
+ newEntry = new JarEntry(entry);
+ } else {
+ newEntry = new JarEntry(name);
+ }
+
+ if(removeEntryTimestamp) {
+ newEntry.setTime(0L);
+ }
+ try {
+ this.jarOutputStream.putNextEntry(newEntry);
+ } catch(Exception duplicate){
+ //类重复了,先忽略吧
+ println "addJar putNextEntry fail: " + name
+ continue;
+ }
+ int count;
+ while((count = zis.read(this.buffer)) != -1) {
+ this.jarOutputStream.write(this.buffer, 0, count);
+ }
+
+ this.jarOutputStream.closeEntry();
+ zis.closeEntry();
+ }
+ }
+ }
+ } catch (JarFilter.ZipAbortException var13) {
+ throw new IOException(var13);
+ } finally {
+ if (zis != null) {
+ zis.close();
+ }
+ if (e != null) {
+ e.close();
+ }
+ }
+
+ }
+
+ public void addEntry(String path, byte[] bytes) throws IOException {
+ this.init();
+ this.jarOutputStream.putNextEntry(new JarEntry(path));
+ this.jarOutputStream.write(bytes);
+ this.jarOutputStream.closeEntry();
+ }
+
+ public void close() throws IOException {
+ if (this.jarOutputStream != null) {
+ jarOutputStream.close();
+ jarOutputStream = null;
+ }
+ if (this.fos != null) {
+ fos.close();
+ fos = null;
+ }
+
+ }
+}
+
+public interface JarFilter {
+ boolean checkEntry(String var1) throws ZipAbortException;
+
+ public static class ZipAbortException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipAbortException() {
+ }
+
+ public ZipAbortException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public ZipAbortException(Throwable cause, String format, Object... args) {
+ super(String.format(format, args), cause);
+ }
+
+ public ZipAbortException(Throwable cause) {
+ super(cause);
+ }
+ }
+}
+
+public class ZipUtil {
+
+ public static List unZip(String path, String savepath) throws IOException
+ {
+ def resultList = new ArrayList();
+ def count = -1;
+ def index = -1;
+ def flag = false;
+ def file = null;
+ def is = null;
+ def fos = null;
+ def bos = null;
+
+ ZipFile zipFile = new ZipFile(path);
+ Enumeration> entries = zipFile.entries();
+
+ while(entries.hasMoreElements())
+ {
+ def buf = new byte[2048];
+ ZipEntry entry = (ZipEntry)entries.nextElement();
+
+ def filename = savepath + entry.getName();
+
+ File folder = new File(filename).getParentFile();
+ if(!folder.exists()){
+ folder.mkdirs();
+ }
+
+ if(!filename.endsWith(File.separator)){
+
+ System.out.println("解压:" + filename);
+
+ file = new File(filename);
+ file.createNewFile();
+ is = zipFile.getInputStream(entry);
+ fos = new FileOutputStream(file);
+ bos = new BufferedOutputStream(fos, 2048);
+
+ while((count = is.read(buf)) > -1)
+ {
+ bos.write(buf, 0, count );
+ }
+
+ bos.flush();
+
+ fos.close();
+ is.close();
+
+ resultList.add(entry.getName());
+ }
+ }
+
+ zipFile.close();
+
+ return resultList
+ }
+
+ /**
+ * 递归压缩文件夹
+ * @param srcRootDir 压缩文件夹根目录的子路径
+ * @param file 当前递归压缩的文件或目录对象
+ * @param zos 压缩文件存储对象
+ * @throws Exception
+ */
+ public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception
+ {
+ if (file == null)
+ {
+ return;
+ }
+
+ //如果是文件,则直接压缩该文件
+ if (file.isFile())
+ {
+ int count, bufferLen = 1024;
+ byte[] data = new byte[bufferLen];
+
+ //获取文件相对于压缩文件夹根目录的子路径
+ String subPath = file.getAbsolutePath();
+ int index = subPath.indexOf(srcRootDir);
+ if (index != -1)
+ {
+ subPath = subPath.substring(srcRootDir.length() + File.separator.length());
+ }
+
+ if (filter.contains(subPath)) {
+ System.out.println("过滤:" + subPath)
+ return;
+ } else {
+ System.out.println("压缩:" + subPath)
+ }
+
+ ZipEntry entry = new ZipEntry(subPath);
+ zos.putNextEntry(entry);
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(data, 0, bufferLen)) != -1)
+ {
+ zos.write(data, 0, count);
+ }
+ bis.close();
+ zos.closeEntry();
+ }
+ //如果是目录,则压缩整个目录
+ else
+ {
+ //压缩目录中的文件或子目录
+ File[] childFileList = file.listFiles();
+ for (int n=0; n filter) throws Exception
+ {
+ CheckedOutputStream cos = null;
+ ZipOutputStream zos = null;
+ try
+ {
+ File srcFile = new File(srcPath);
+
+ //判断压缩文件保存的路径是否存在,如果不存在,则创建目录
+ File zipDir = new File(zipPath);
+ if (!zipDir.exists() || !zipDir.isDirectory())
+ {
+ zipDir.mkdirs();
+ }
+
+ //创建压缩文件保存的文件对象
+ String zipFilePath = zipPath + File.separator + zipFileName;
+ File zipFile = new File(zipFilePath);
+ if (zipFile.exists())
+ {
+ ///删除已存在的目标文件
+ boolean success = zipFile.delete();
+ System.out.println("删除已存在的目标文件:" + success)
+
+ }
+
+ cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
+ zos = new ZipOutputStream(cos);
+
+ //如果只是压缩一个文件,则需要截取该文件的父目录
+ String srcRootDir = srcPath;
+ if (srcFile.isFile())
+ {
+ int index = srcPath.lastIndexOf(File.separator);
+ if (index != -1)
+ {
+ srcRootDir = srcPath.substring(0, index);
+ }
+ }
+ //调用递归压缩方法进行目录或文件压缩
+ zip(srcRootDir, srcFile, zos, filter);
+ zos.flush();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ try
+ {
+ if (zos != null)
+ {
+ zos.close();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/FairyPlugin/agp7_0_3/plugin.gradle b/FairyPlugin/agp7_0_3/plugin.gradle
new file mode 100644
index 00000000..30e731ff
--- /dev/null
+++ b/FairyPlugin/agp7_0_3/plugin.gradle
@@ -0,0 +1,672 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.security.MessageDigest
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.androidResources.additionalParameters(
+ '-I', currentSelectedBar,
+ '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用
+ '--allow-reserved-package-id',
+ '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties")
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ //读取宿主编译信息
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ props.keySet().forEach { propKey->
+ project.ext.setProperty(propKey, props.getProperty(propKey))
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+ //应用宿主的主题包
+ applyThemePatch(varName, varDirName, hostBarExtraRootDir);
+ }
+
+ if (isApplicationModule) {
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+ tasks.findByName("compile${variant.name.capitalize()}Sources").doLast {
+ project.copy {
+ from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) {
+ include "classes.jar"
+ }
+ into(buildDir.absolutePath + "/outputs")
+ rename('classes', project.name + "-" + varName)
+ }
+ }
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ def hostVersionName = project.ext.getProperty("host.versionName")
+ def hostVersionCode = project.ext.getProperty("host.versionCode")
+ def hostApplicationId = project.ext.getProperty("host.applicationId")
+ def hostBuildType = project.ext.getProperty("host.buildType")
+ def hostFlavorName = project.ext.getProperty("host.flavorName")
+ def hostVarName = project.ext.getProperty("host.varName")
+ def hostVarDirName = project.ext.getProperty("host.varDirName")
+ if (hostApplicationId != null) {
+ projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName)
+ } else {
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir)
+ } else {
+ throw new Exception("宿主基线包信息丢失")
+ }
+ }
+
+ fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest:" + manifestFile.absolutePath
+ println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode
+ println "插入宿主ID hostApplicationId=" + hostApplicationId
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ (packageName + ":" + line.split([":"])[1])
+ .replace("public_static_final_host", "public_static_final_plugin")
+ .replace("0x7f3", "0x7f0")
+ .replace("0x7f4", "0x7f1")
+ }
+ }
+ }
+ }
+
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8")
+ if (proguardTask != null) {
+ if (project.ext.has("host.varName")) {
+ def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt";
+ println "发现基线mapping文件: " + baselineMappingFile
+ }
+ proguardTask.doLast {
+ println "混淆完成 " + varName.capitalize()
+ def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8"
+ if (!new File(host_obfuscated_dex).exists()) {
+ project.logger.error "宿主基线混淆包不存在 " + host_obfuscated_dex
+ throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件")
+ }
+ println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex
+
+ outputs.files.files.each { File file->
+ if (file.absolutePath.endsWith(proguardTask.name)) {
+ println "file outputs=>${file.absolutePath}"
+ diffDexes(file.absolutePath, host_obfuscated_dex)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+def String curl(String url, String md5, String dir) {
+ File file = new File(dir, url.substring(url.lastIndexOf("/") + 1))
+ if (file.exists() && file.length() > 0) {
+ return file.absolutePath
+ }
+ println "downloading " + url
+ new File(dir).mkdirs();
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5")
+ messageDigest.reset()
+ try {
+ HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection())
+ InputStream inputStream = httpConn.getInputStream()
+ OutputStream ouput =new FileOutputStream(file)
+ byte[] buffer = new byte[8*1024]
+ int size
+ while((size = inputStream.read(buffer)) != -1) {
+ ouput.write(buffer, 0, size)
+ messageDigest.update(buffer, 0, size)
+ }
+ ouput.close()
+ httpConn.disconnect()
+ } catch(Exception){}
+ byte[] bs = messageDigest.digest()
+ StringBuilder stringBuilder = new StringBuilder()
+ for(byte b : bs) {
+ String hex = Integer.toHexString(b & 0xFF)
+ if (hex.length() < 2) {
+ stringBuilder.append("0")
+ }
+ stringBuilder.append(hex)
+ }
+ if (!md5.equalsIgnoreCase(stringBuilder.toString())) {
+ println "download fail: " + url + " " + stringBuilder.toString()
+ file.delete()
+ } else {
+ println "download success: " + url + " " + stringBuilder.toString()
+ }
+ return file.absolutePath
+}
+def ArrayList basksmali(String exe, String src, String dest) throws Exception
+{
+ ArrayList result = new ArrayList<>()
+ new File(dest).mkdirs()
+ File srcDir = new File(src);
+ for (File dex : srcDir.listFiles()) {
+ var cmd = ("java -jar " + exe + " " +
+ "d " +
+ "-o " + dest + "/" + dex.name + " " +
+ dex.absolutePath)
+ consumeStream(cmd.execute().getInputStream())
+ fileTree(dest + "/" + dex.name).forEach { it->
+ var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "")
+ result.add(relativeFile)
+ }
+ }
+ return result;
+}
+def void smali(String exe, String src, String dest) throws Exception
+{
+ for (File f : file(src).listFiles()) {
+ file(dest + "/" + f.name).delete()
+ var cmd = ("java -jar " + exe + " " +
+ "a " +
+ "-o " + dest + "/" + f.name + " " +
+ f.absolutePath)
+ println cmd
+ consumeStream(cmd.execute().getInputStream())
+ }
+}
+def consumeStream(InputStream is) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()
+ final byte[] buffer = new byte[256]
+ int len
+ try {
+ while ((len = is.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, len)
+ }
+ } catch(IOException){}
+ finally {
+ String msg = outputStream.toString()
+ if (msg != null && msg.length() > 0) {
+ println "***exe msg begin***"
+ println msg
+ println "***exe msg end***"
+ }
+ try {
+ outputStream.close()
+ is.close()
+ } catch(Exception){}
+ }
+}
+def deleteDuplicated(String plugin, ArrayList hostClasses) {
+ for(File dexDir : file(plugin).listFiles()) {
+ fileTree(dexDir.absolutePath).forEach {
+ var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "")
+ if (hostClasses.contains(relativeFile)) {
+ it.delete()
+ }
+ }
+ }
+}
+def diffDexes(String plugin, String host) {
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+ println "插件:" + plugin
+ println "宿主:" + host
+ var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools')
+ var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools')
+ basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin")
+ ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host")
+ deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses)
+ smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin)
+}
+
+def applyThemePatch(varName, varDir, hostPatchExtractDir) {
+ println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir
+
+ if (!file(hostPatchExtractDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir);
+ }
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName")
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir"
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ file(resourceDir).mkdirs()
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '**/*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+ file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs()
+
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile")
+ "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile".execute().waitFor()
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet();
+
+ HashMap enumItemList = new HashMap();
+
+ HashMap> attrFlagMap = new HashMap>();
+
+ public StyleProcessor(File destFile) {
+ this.destFile = destFile;
+
+ SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+ outXmlHandler = factory.newTransformerHandler();
+
+ }
+
+ public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) {
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ StyleProcessor processor = new StyleProcessor(destFile);
+ BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8"));
+ InputSource inputSource = new InputSource(br)
+ saxParser.parse(inputSource, processor);
+ return processor;
+ } catch (ParserConfigurationException e) {
+ System.out.println(e.getMessage());
+ } catch (SAXException e) {
+ System.out.println(e.getMessage());
+ } catch (FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ return null;
+ }
+
+ ////////////////
+ ////////////////
+ ////////////////
+
+ public void startDocument() throws SAXException {
+ try {
+ javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")));
+ char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray();
+ outXmlHandler.comment(common, 0, common.length);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
+
+ boolean skip = false;
+ if (!qName.equals("declare-styleable")) {
+ String space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ String tag = space + "<" + qName;
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\"";
+ }
+ tag = tag + ">";
+
+ if (qName.equals("attr") && stack.size() == 2) {
+ String parentDecalreStyleName = attrList.lastKey();
+ attrList.get(parentDecalreStyleName).add(attributes.getValue("name"));
+ }
+
+ if (qName.equals("enum") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\"";
+ String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\"";
+ println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size();
+ enumItemList.put(regx, regValue)
+
+ }
+
+ if (qName.endsWith("flag") && stack.size() == 3) {
+
+ Node n3 = stack.get(2);
+ String attr = n3.name;
+
+ String flagName = attributes.getValue("name");
+ String flagValue = attributes.getValue("value");
+
+ HashMap item = attrFlagMap.get(attr);
+ if (item == null) {
+ item = new HashMap();
+ attrFlagMap.put(attr, item)
+ }
+
+ println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}"
+
+ item.put(flagName, flagValue);
+ }
+
+ if (qName.equals("attr")) {
+ if (!attrSets.contains(attributes.getValue("name"))) {
+ attrSets.add(attributes.getValue("name"));
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ skip = true;
+ }
+ } else {
+ try {
+ outXmlHandler.startElement(uri, localName, qName, attributes)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //declare-styleable的name属性
+ attrList.put(attributes.getValue("name"), new ArrayList());
+ }
+
+ if (!stack.empty()) {
+ Node top = stack.peek();
+ top.hasChild = true;
+ }
+ stack.add(new Node(attributes.getValue("name"), false, skip));
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+
+ Node node = stack.pop();
+ if (node.skip) {
+ return;
+ }
+
+ if (!qName.equals("declare-styleable")) {
+ String space = "";
+ if (node.hasChild) {
+ space = "\n";
+ for (int i = 0; i < stack.size(); i++) {
+ space = space + " ";
+ }
+ }
+ try {
+ outXmlHandler.endElement(uri, localName, qName)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ Node node = stack.peek();
+ if (node.skip) {
+ return;
+ }
+
+ try {
+ outXmlHandler.characters(ch, start, length)
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void endDocument() throws SAXException {
+ try {
+ outXmlHandler.endDocument();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class Node {
+ String name = null;
+ boolean hasChild = false;
+ boolean skip = false;
+
+ public Node(String name, boolean hasChild, boolean skip) {
+ this.name = name;
+ this.hasChild = hasChild;
+ this.skip = skip;
+ }
+ }
+
+ public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) {
+
+ String localText = xmlText;
+
+ Iterator>> flagItr = attrFlagMap.entrySet().iterator();
+
+ while (flagItr.hasNext()) {
+ Map.Entry> entry = flagItr.next();
+
+ HashMap flagValueMap = entry.getValue();
+ String attrName = entry.getKey();
+
+ String regx = ":" + attrName + "\\s*=\\s*\".+?\"";
+
+ Matcher matcher = Pattern.compile(regx).matcher(localText);
+
+ HashMap flagsMap = new HashMap();
+
+ while(matcher.find()) {
+
+ String text0 = matcher.group(0);
+
+ String flagValue = text0.split("=")[1].trim().replaceAll("\"", "");
+ String[] flags = flagValue.split("\\|");
+
+ Integer flagIntValue = null;
+ for(String flag: flags) {
+
+ String intFlag = flagValueMap.get(flag);
+ int definedValue;
+ if (intFlag.startsWith("0x")) {
+ //16进制
+ definedValue = Integer.valueOf(intFlag.substring(2), 16);
+ } else {
+ //10进制
+ definedValue = Integer.valueOf(intFlag);
+ }
+
+ if (flagIntValue == null) {
+ flagIntValue = definedValue;
+ } else {
+ flagIntValue = flagIntValue | definedValue;
+ }
+ }
+
+ String text0ed = ":" + attrName + "=\"" + flagIntValue + "\"";
+ text0 = text0.replaceAll("\\|", "\\\\|");//正则转义
+
+ println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed
+
+ flagsMap.put(text0, text0ed);
+ }
+
+ Iterator> iterator = flagsMap.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry stringEntry = iterator.next();
+ localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue());
+ }
+ }
+
+ return localText;
+ }
+
+}
diff --git a/FairyPlugin/agp7_1_3/host.gradle b/FairyPlugin/agp7_1_3/host.gradle
new file mode 100644
index 00000000..47ff37b3
--- /dev/null
+++ b/FairyPlugin/agp7_1_3/host.gradle
@@ -0,0 +1 @@
+apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_0_3/host.gradle"
\ No newline at end of file
diff --git a/FairyPlugin/agp7_1_3/plugin.gradle b/FairyPlugin/agp7_1_3/plugin.gradle
new file mode 100644
index 00000000..0921b4f6
--- /dev/null
+++ b/FairyPlugin/agp7_1_3/plugin.gradle
@@ -0,0 +1,672 @@
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.DefaultHandler
+
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.parsers.SAXParser
+import javax.xml.parsers.SAXParserFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.sax.SAXTransformerFactory
+import javax.xml.transform.sax.TransformerHandler
+import javax.xml.transform.stream.StreamResult
+import java.security.MessageDigest
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+configurations {
+ baselinePatch
+ debugBaselinePatch
+ releaseBaselinePatch
+}
+
+class FaiyExt {
+ private boolean requireHostVersion = true;
+
+ public boolean getRequireHostVersion() {
+ return requireHostVersion
+ }
+
+ public void setRequireHostVersion(boolean required) {
+ this.requireHostVersion = required
+ }
+}
+extensions.create('fairy', FaiyExt)
+
+android{
+}
+
+def hostBar = "host.bar"
+def hostJar = "host_classes.jar"
+def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/'
+def defaultExtraDir = hostBarRootDir + "unspecified"
+def currentSelectedBar = defaultExtraDir + "/" + hostBar
+gradle.startParameter.taskNames.each { startTaskName ->
+ if (startTaskName.contains("Debug")) {
+ currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar
+ } else if (startTaskName.contains("Release")) {
+ currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar
+ }
+}
+
+println "执行命令决定了bar文件依赖路径!"
+println ">>>" + hostBarRootDir
+println ">>>" + currentSelectedBar
+
+android.androidResources.additionalParameters(
+ '-I', currentSelectedBar,
+ '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用
+ '--allow-reserved-package-id',
+ '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties")
+
+afterEvaluate {
+ def isApplicationModule = getPlugins().hasPlugin("com.android.application")
+ if (isApplicationModule) {
+ if (android.defaultConfig.applicationId == null) {
+ throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId")
+ }
+ }
+ def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants
+ for (variant in moduleVariants) {
+ def varName = variant.name;
+ def buildTypeName = variant.buildType.name
+ def flavorName = variant.flavorName
+ def varDirName = variant.dirName
+ def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name
+
+ println '\n'
+ println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName
+
+ //读取宿主基线包文件路径,并解压到指定目录
+ def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch')
+ if (configure == null || configure.files.size() == 0) {
+ configure = configurations['baselinePatch']
+ }
+ if (configure.files.size() == 0) {
+ project.logger.error '未配置基线包依赖!'
+ }
+ configure.files.each { patch ->
+ println "发现宿主基线配置指向位置:" + patch.absolutePath
+ //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor
+ if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) {
+ println "宿主基线patch文件不存在:" + patch.absolutePath
+ println "清理对应的缓存目录:" + hostBarExtraRootDir
+ file(hostBarExtraRootDir).deleteDir();
+ } else {
+ //按buildType解压到不同的文件夹里面
+ println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir
+ //这里做一个简单校验判断,提高效率
+ if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ def zipFile = file(patch.absolutePath)
+ def outputDir = file(hostBarExtraRootDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ } else {
+ println "源与目标文件大小相同,省去解压步骤"
+ }
+ //这里做一个简单校验判断,提高效率
+ if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) {
+ copy {
+ //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹
+ //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖
+ def zipFile = file(patch.absolutePath);
+ def outputDir = file(defaultExtraDir)
+ outputDir.deleteDir();
+ from zipTree(zipFile)
+ from(zipFile) {
+ rename {
+ hostBar
+ }
+ }
+ into outputDir
+ }
+ }
+ //读取宿主编译信息
+ File propFile = file(hostBarExtraRootDir + "/HostInfo.prop");
+ if (!propFile.exists()) {
+ throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath)
+ }
+ def Properties props = new Properties()
+ props.load(new FileInputStream(propFile))
+ props.keySet().forEach { propKey->
+ project.ext.setProperty(propKey, props.getProperty(propKey))
+ }
+ }
+ }
+ //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar
+ //这里偷懒了,只考虑了buildType,忽略favor
+ def hostClassesJar = hostBarExtraRootDir + "/" + hostJar
+ if (!file(hostClassesJar).exists()) {
+ //当不是使用assembleDebug和assembleRelease编译时
+ println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default"
+ hostClassesJar = defaultExtraDir + "/" + hostJar;
+ }
+ println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled
+ if (!variant.buildType.minifyEnabled) {
+ configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ } else {
+ configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar)))
+ }
+
+ def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources");
+ mergeResourceTask.setOnlyIf { true }
+ mergeResourceTask.outputs.upToDateWhen { false }
+ mergeResourceTask.doLast {
+ //应用宿主的主题包
+ applyThemePatch(varName, varDirName, hostBarExtraRootDir);
+ }
+
+ if (isApplicationModule) {
+ tasks.findByName("bundle${varName.capitalize()}Classes").doLast {
+ project.copy {
+ from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) {
+ include "classes.jar"
+ }
+ into(buildDir.absolutePath + "/outputs")
+ rename('classes', project.name + "-" + varName)
+ }
+ }
+ tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes")
+ tasks.findByName("process${varName.capitalize()}Manifest").doLast {
+ def hostVersionName = project.ext.getProperty("host.versionName")
+ def hostVersionCode = project.ext.getProperty("host.versionCode")
+ def hostApplicationId = project.ext.getProperty("host.applicationId")
+ def hostBuildType = project.ext.getProperty("host.buildType")
+ def hostFlavorName = project.ext.getProperty("host.flavorName")
+ def hostVarName = project.ext.getProperty("host.varName")
+ def hostVarDirName = project.ext.getProperty("host.varDirName")
+ if (hostApplicationId != null) {
+ projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName)
+ } else {
+ if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) {
+ throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir)
+ } else {
+ throw new Exception("宿主基线包信息丢失")
+ }
+ }
+
+ fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile->
+ if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) {
+ println "插件Manifest:" + manifestFile.absolutePath
+ println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName
+ println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode
+ println "插入宿主ID hostApplicationId=" + hostApplicationId
+ def originManifestContent = manifestFile.getText('UTF-8')
+ def modifyedManifestContent = originManifestContent.replace("
+ (packageName + ":" + line.split([":"])[1])
+ .replace("public_static_final_host", "public_static_final_plugin")
+ .replace("0x7f3", "0x7f0")
+ .replace("0x7f4", "0x7f1")
+ }
+ }
+ }
+ }
+
+ //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用
+ def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8")
+ if (proguardTask != null) {
+ if (project.ext.has("host.varName")) {
+ def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt";
+ println "发现基线mapping文件: " + baselineMappingFile
+ }
+ proguardTask.doLast {
+ println "混淆完成 " + varName.capitalize()
+ def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8"
+ if (!new File(host_obfuscated_dex).exists()) {
+ project.logger.error "宿主基线混淆包不存在 " + host_obfuscated_dex
+ throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件")
+ }
+ println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex
+
+ outputs.files.files.each { File file->
+ if (file.absolutePath.endsWith(proguardTask.name)) {
+ println "file outputs=>${file.absolutePath}"
+ diffDexes(file.absolutePath, host_obfuscated_dex)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+def String curl(String url, String md5, String dir) {
+ File file = new File(dir, url.substring(url.lastIndexOf("/") + 1))
+ if (file.exists() && file.length() > 0) {
+ return file.absolutePath
+ }
+ println "downloading " + url
+ new File(dir).mkdirs();
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5")
+ messageDigest.reset()
+ try {
+ HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection())
+ InputStream inputStream = httpConn.getInputStream()
+ OutputStream ouput =new FileOutputStream(file)
+ byte[] buffer = new byte[8*1024]
+ int size
+ while((size = inputStream.read(buffer)) != -1) {
+ ouput.write(buffer, 0, size)
+ messageDigest.update(buffer, 0, size)
+ }
+ ouput.close()
+ httpConn.disconnect()
+ } catch(Exception){}
+ byte[] bs = messageDigest.digest()
+ StringBuilder stringBuilder = new StringBuilder()
+ for(byte b : bs) {
+ String hex = Integer.toHexString(b & 0xFF)
+ if (hex.length() < 2) {
+ stringBuilder.append("0")
+ }
+ stringBuilder.append(hex)
+ }
+ if (!md5.equalsIgnoreCase(stringBuilder.toString())) {
+ println "download fail: " + url + " " + stringBuilder.toString()
+ file.delete()
+ } else {
+ println "download success: " + url + " " + stringBuilder.toString()
+ }
+ return file.absolutePath
+}
+def ArrayList basksmali(String exe, String src, String dest) throws Exception
+{
+ ArrayList result = new ArrayList<>()
+ new File(dest).mkdirs()
+ File srcDir = new File(src);
+ for (File dex : srcDir.listFiles()) {
+ var cmd = ("java -jar " + exe + " " +
+ "d " +
+ "-o " + dest + "/" + dex.name + " " +
+ dex.absolutePath)
+ consumeStream(cmd.execute().getInputStream())
+ fileTree(dest + "/" + dex.name).forEach { it->
+ var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "")
+ result.add(relativeFile)
+ }
+ }
+ return result;
+}
+def void smali(String exe, String src, String dest) throws Exception
+{
+ for (File f : file(src).listFiles()) {
+ file(dest + "/" + f.name).delete()
+ var cmd = ("java -jar " + exe + " " +
+ "a " +
+ "-o " + dest + "/" + f.name + " " +
+ f.absolutePath)
+ println cmd
+ consumeStream(cmd.execute().getInputStream())
+ }
+}
+def consumeStream(InputStream is) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()
+ final byte[] buffer = new byte[256]
+ int len
+ try {
+ while ((len = is.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, len)
+ }
+ } catch(IOException){}
+ finally {
+ String msg = outputStream.toString()
+ if (msg != null && msg.length() > 0) {
+ println "***exe msg begin***"
+ println msg
+ println "***exe msg end***"
+ }
+ try {
+ outputStream.close()
+ is.close()
+ } catch(Exception){}
+ }
+}
+def deleteDuplicated(String plugin, ArrayList hostClasses) {
+ for(File dexDir : file(plugin).listFiles()) {
+ fileTree(dexDir.absolutePath).forEach {
+ var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "")
+ if (hostClasses.contains(relativeFile)) {
+ it.delete()
+ }
+ }
+ }
+}
+def diffDexes(String plugin, String host) {
+ println "通过基线混淆包和插件混淆包做diff清理重复的class"
+ println "插件:" + plugin
+ println "宿主:" + host
+ var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools')
+ var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools')
+ basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin")
+ ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host")
+ deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses)
+ smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin)
+}
+
+def applyThemePatch(varName, varDir, hostPatchExtractDir) {
+ println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir
+
+ if (!file(hostPatchExtractDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir);
+ }
+
+ def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName")
+ def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir"
+
+ if (!file(fromDir).exists()) {
+ throw new FileNotFoundException("Dir Not Found: " + fromDir);
+ }
+
+ if (!file(resourceDir).exists()) {
+ file(resourceDir).mkdirs()
+ }
+
+ FileTree allxmlFiles = fileTree(dir: fromDir)
+ allxmlFiles.include '**/*.xml'
+
+ if (allxmlFiles.size() == 0) {
+ println fromDir + " 目录未生成,请先编译宿主!!"
+ throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir);
+ }
+
+ allxmlFiles.each { File itemFile ->
+ file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs()
+
+ File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion())
+ String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath
+
+ projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile")
+ "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile".execute().waitFor()
+ }
+}
+
+public class StyleProcessor extends DefaultHandler {
+
+ File destFile;
+ Stack stack = new Stack();
+ TransformerHandler outXmlHandler;
+
+ SortedMap> attrList = new TreeMap>();
+ HashSet attrSets = new HashSet