diff --git a/README.md b/README.md index 8dd9529e..f8225241 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Android API 9(2.3)+ ; android-gradle-build 2.0.0+ - 2、fastdex会忽略开启混淆的buildType -- 3、强烈建议你的application不要直接依赖library工程,打成aar包让application工程远程依赖 +- 3、强烈建议你的application不要直接依赖library工程,打成aar包让application工程远程依赖,目前还没有做充分测试 -- 4、开启自定义的编译任务能获得更快的构建速度,对使用了butterknife的大型项目效果最明显,这一特性目前还不稳定0.0.3-beta3后默认关闭了,使用了,如果想尝试在build.gradle中加入下面配置 +- 4、开启自定义的编译任务能获得更快的构建速度(这个特性目前不支持使用lambda),对使用了butterknife的大型项目效果最明显,这一特性目前还不稳定0.0.3-beta3后默认关闭了,使用了,如果想尝试在build.gradle中加入下面配置 ```` fastdex { @@ -101,4 +101,4 @@ Android API 9(2.3)+ ; android-gradle-build 2.0.0+ [Android应用程序资源的编译和打包过程分析](http://blog.csdn.net/luoshengyang/article/details/8744683) - + \ No newline at end of file diff --git a/bintray.gradle b/bintray.gradle index 72d14a16..ec8c254a 100644 --- a/bintray.gradle +++ b/bintray.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.jfrog.bintray' apply plugin: 'maven-publish' -def projectName = project.name +def projectName = project.hasProperty("MAVEN_ARTIFACT_ID") ? project.MAVEN_ARTIFACT_ID : project.name def mavenDesc = projectName def baseUrl = 'https://github.com/typ0520/fastdex' def siteUrl = baseUrl @@ -86,7 +86,7 @@ bintray { configurations = ['archives'] pkg { repo = 'maven' - name = "${project.group}:${projectName}" + name = "${project.groupId}:${projectName}" desc = mavenDesc websiteUrl = siteUrl issueTrackerUrl = issueUrl diff --git a/build.gradle b/build.gradle index edf54162..d0ef9189 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' classpath 'com.github.dcendents:android-maven-plugin:1.2' diff --git a/fastdex-build-lib/.gitignore b/fastdex-build-lib/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/fastdex-build-lib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/fastdex-build-lib/build.gradle b/fastdex-build-lib/build.gradle new file mode 100644 index 00000000..0dcdde39 --- /dev/null +++ b/fastdex-build-lib/build.gradle @@ -0,0 +1,61 @@ +apply plugin: 'java' +apply plugin: 'maven' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +[compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' + +dependencies { + compile project(':fastdex-common') + compile 'org.apache.ant:ant:1.8.2' + compile 'com.google.guava:guava:18.0' + compile 'com.android.tools.ddms:ddmlib:25.3.1' + + testCompile 'junit:junit:4.12' + testCompile 'com.google.code.gson:gson:2.3.1' +} + +sourceSets { + main { + java { + srcDir 'src/main/java' + } + + resources { + srcDir 'src/main/resources' + } + } +} + +apply from: rootProject.file('bintray.gradle') + + +def generated = new File("${project.buildDir}/generated/java") +sourceSets { + main { + java { + srcDir generated + } + } +} + +task generateVersionConstantsJava { + inputs.property("version", version) + ext.versionFile = new File(generated, "com/github/typ0520/fastdex/Version.java") + outputs.file(versionFile) +} + +generateVersionConstantsJava << { + versionFile.parentFile.mkdirs() + versionFile.text = """ +package com.github.typ0520.fastdex; + +public final class Version { + private Version() {} + public static final String FASTDEX_BUILD_VERSION = "$version"; +} +""" +} + +tasks.compileJava.dependsOn generateVersionConstantsJava \ No newline at end of file diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptResourceCollector.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptResourceCollector.java new file mode 100644 index 00000000..228a111b --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptResourceCollector.java @@ -0,0 +1,354 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import com.google.common.base.Joiner; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class AaptResourceCollector { + + private final Map>> rTypeResourceDirectoryMap; + //private final Map> rTypeIncreaseResourceDirectoryListMap; +// private final Map> rTypeIncreaseResourceDirectoryMap; + private final Map rTypeEnumeratorMap; + private final Map originalResourceMap; + private final Map> rTypeResourceMap; + private final Map> rTypeIncreaseResourceMap; + private final Map> duplicateResourceMap; + private final Map> sanitizeTypeMap; + private final Set ignoreIdSet; + private int currentTypeId; + + public AaptResourceCollector() { + this.rTypeResourceDirectoryMap = new HashMap>>(); +// this.rTypeIncreaseResourceDirectoryListMap = new HashMap>(); +// this.rTypeIncreaseResourceDirectoryMap = new HashMap>(); + this.rTypeEnumeratorMap = new HashMap(); + this.rTypeResourceMap = new HashMap>(); + this.rTypeIncreaseResourceMap = new HashMap>(); + this.duplicateResourceMap = new HashMap>(); + this.sanitizeTypeMap = new HashMap>(); + this.originalResourceMap = new HashMap(); + this.ignoreIdSet = new HashSet(); + //attr type must 1 + this.currentTypeId = 2; + } + + public AaptResourceCollector(Map> rTypeResourceMap) { + this(); + if (rTypeResourceMap != null) { + Iterator>> iterator = rTypeResourceMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + RDotTxtEntry.RType rType = entry.getKey(); + Set set = entry.getValue(); +// this.rTypeResourceMap.put(rType, new HashSet(set)); + for (RDotTxtEntry rDotTxtEntry : set) { + originalResourceMap.put(rDotTxtEntry, rDotTxtEntry); + ResourceIdEnumerator resourceIdEnumerator = null; + if (!rDotTxtEntry.idType.equals(RDotTxtEntry.IdType.INT_ARRAY)) { + int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue(); + int typeId = ((resourceId & 0x00FF0000) / 0x00010000); + if (typeId >= currentTypeId) { + currentTypeId = typeId + 1; + } + if (this.rTypeEnumeratorMap.containsKey(rType)) { + resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType); + if (resourceIdEnumerator.currentId < resourceId) { + resourceIdEnumerator.currentId = resourceId; + } + } else { + resourceIdEnumerator = new ResourceIdEnumerator(); + resourceIdEnumerator.currentId = resourceId; + this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator); + } + } + } + } + } + } + + public void addIntResourceIfNotPresent(RDotTxtEntry.RType rType, String name) { //, ResourceDirectory resourceDirectory) { + if (!rTypeEnumeratorMap.containsKey(rType)) { + if (rType.equals(RDotTxtEntry.RType.ATTR)) { + rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1)); + } else { + rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++)); + } + } + + RDotTxtEntry entry = new FakeRDotTxtEntry(RDotTxtEntry.IdType.INT, rType, name); + Set resourceSet = null; + if (this.rTypeResourceMap.containsKey(rType)) { + resourceSet = this.rTypeResourceMap.get(rType); + } else { + resourceSet = new HashSet(); + this.rTypeResourceMap.put(rType, resourceSet); + } + if (!resourceSet.contains(entry)) { + String idValue = String.format("0x%08x", rTypeEnumeratorMap.get(rType).next()); + addResource(rType, RDotTxtEntry.IdType.INT, name, idValue); //, resourceDirectory); + } + } + + public void addIntArrayResourceIfNotPresent(RDotTxtEntry.RType rType, String name, int numValues) { + // Robolectric expects the array to be populated with the right number + // of values, irrespective + // of what the values are. + String idValue = String.format("{ %s }", Joiner.on(",").join(Collections.nCopies(numValues, "0x7f000000"))); + addResource(rType, RDotTxtEntry.IdType.INT_ARRAY, name, idValue); + } + + /** + * add resource + * + * @param rType + * @param idType + * @param name + * @param idValue + */ + public void addResource(RDotTxtEntry.RType rType, RDotTxtEntry.IdType idType, String name, String idValue) { + Set resourceSet = null; + if (this.rTypeResourceMap.containsKey(rType)) { + resourceSet = this.rTypeResourceMap.get(rType); + } else { + resourceSet = new HashSet(); + this.rTypeResourceMap.put(rType, resourceSet); + } + RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue); + boolean increaseResource = false; + if (!resourceSet.contains(rDotTxtEntry)) { + if (this.originalResourceMap.containsKey(rDotTxtEntry)) { + this.rTypeEnumeratorMap.get(rType).previous(); + rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry); + } else { + increaseResource = true; + } + resourceSet.add(rDotTxtEntry); + } + Set increaseResourceSet = null; + //new r dot txt entry + if (this.rTypeIncreaseResourceMap.containsKey(rType)) { + increaseResourceSet = this.rTypeIncreaseResourceMap.get(rType); + } else { + increaseResourceSet = new HashSet(); + this.rTypeIncreaseResourceMap.put(rType, increaseResourceSet); + } + if (increaseResource) { + increaseResourceSet.add(rDotTxtEntry); +//addResourceDirectory(rType, name, resourceDirectory); + } + } + +//private void addResourceDirectory(RType rType,String name, ResourceDirectory resourceDirectory){ +//if(resourceDirectory!=null){ +//Map resourceDirectoryMap=null; +//List resourceDirectoryList=null; +//if(this.rTypeIncreaseResourceDirectoryMap.containsKey(rType)){ +//resourceDirectoryMap=this.rTypeIncreaseResourceDirectoryMap.get(rType); +//resourceDirectoryList=this.rTypeIncreaseResourceDirectoryListMap.get(rType); +//}else{ +//resourceDirectoryMap=new HashMap(); +//this.rTypeIncreaseResourceDirectoryMap.put(rType, resourceDirectoryMap); +//resourceDirectoryList=new ArrayList(); +//this.rTypeIncreaseResourceDirectoryListMap.put(rType, resourceDirectoryList); +//} +//ResourceDirectory existResourceDirectory=null; +//if(resourceDirectoryMap.containsKey(resourceDirectory)){ +//existResourceDirectory=resourceDirectoryMap.get(resourceDirectory); +//}else{ +//existResourceDirectory=resourceDirectory; +//resourceDirectoryMap.put(resourceDirectory, resourceDirectory); +//resourceDirectoryList.add(existResourceDirectory); +//} +//existResourceDirectory.resourceEntrySet.add(new ResourceEntry(name,null)); +//} +//} + + /** + * is contain resource + * + * @param rType + * @param idType + * @param name + * @return boolean + */ + public boolean isContainResource(RDotTxtEntry.RType rType, RDotTxtEntry.IdType idType, String name) { + boolean result = false; + if (this.rTypeResourceMap.containsKey(rType)) { + Set resourceSet = this.rTypeResourceMap.get(rType); + if (resourceSet.contains(new RDotTxtEntry(idType, rType, name, "0x7f000000"))) { + result = true; + } + } + return result; + } + + /** + * add r type resource name + * + * @param rType + * @param resourceName + * @param resourceDirectory + */ + void addRTypeResourceName(RDotTxtEntry.RType rType, String resourceName, String resourceValue, ResourceDirectory resourceDirectory) { + Map> directoryResourceDirectoryMap = null; + if (this.rTypeResourceDirectoryMap.containsKey(rType)) { + directoryResourceDirectoryMap = this.rTypeResourceDirectoryMap.get(rType); + } else { + directoryResourceDirectoryMap = new HashMap>(); + this.rTypeResourceDirectoryMap.put(rType, directoryResourceDirectoryMap); + } + Set resourceDirectorySet = null; + if (directoryResourceDirectoryMap.containsKey(resourceDirectory.directoryName)) { + resourceDirectorySet = directoryResourceDirectoryMap.get(resourceDirectory.directoryName); + } else { + resourceDirectorySet = new HashSet(); + directoryResourceDirectoryMap.put(resourceDirectory.directoryName, resourceDirectorySet); + } + boolean find = false; + ResourceDirectory newResourceDirectory = new ResourceDirectory(resourceDirectory.directoryName, resourceDirectory.resourceFullFilename); + if (!resourceDirectorySet.contains(newResourceDirectory)) { + resourceDirectorySet.add(newResourceDirectory); + } + for (ResourceDirectory oldResourceDirectory : resourceDirectorySet) { + if (oldResourceDirectory.resourceEntrySet.contains(new ResourceEntry(resourceName, resourceValue))) { + find = true; + String resourceKey = rType + "/" + resourceDirectory.directoryName + "/" + resourceName; + Set fullFilenameSet = null; + if (!this.duplicateResourceMap.containsKey(resourceKey)) { + fullFilenameSet = new HashSet(); + fullFilenameSet.add(oldResourceDirectory.resourceFullFilename); + this.duplicateResourceMap.put(resourceKey, fullFilenameSet); + } else { + fullFilenameSet = this.duplicateResourceMap.get(resourceKey); + } + fullFilenameSet.add(resourceDirectory.resourceFullFilename); + } + } + if (!find) { + for (ResourceDirectory oldResourceDirectory : resourceDirectorySet) { + if (oldResourceDirectory.equals(newResourceDirectory)) { + if (!oldResourceDirectory.resourceEntrySet.contains(new ResourceEntry(resourceName, resourceValue))) { + oldResourceDirectory.resourceEntrySet.add(new ResourceEntry(resourceName, resourceValue)); + } + } + } + } + } + + void putSanitizeName(RDotTxtEntry.RType rType, String sanitizeName, String rawName) { + HashMap sanitizeNameMap; + if (!sanitizeTypeMap.containsKey(rType)) { + sanitizeNameMap = new HashMap<>(); + sanitizeTypeMap.put(rType, sanitizeNameMap); + } else { + sanitizeNameMap = sanitizeTypeMap.get(rType); + } + if (!sanitizeNameMap.containsKey(sanitizeName)) { + sanitizeNameMap.put(sanitizeName, rawName); + } + } + + /** + * get raw name + * + * @param sanitizeName + * @return String + */ + public String getRawName(RDotTxtEntry.RType rType, String sanitizeName) { + if (!sanitizeTypeMap.containsKey(rType)) { + return null; + } + return this.sanitizeTypeMap.get(rType).get(sanitizeName); + } + + /** + * get r type resource map + * + * @return Map> + */ + public Map> getRTypeResourceMap() { + return this.rTypeResourceMap; + } + + /** + * @return the duplicateResourceMap + */ + public Map> getDuplicateResourceMap() { + return duplicateResourceMap; + } + + /** + * @return the rTypeIncreaseResourceMap + */ + public Map> getRTypeIncreaseResourceMap() { + return rTypeIncreaseResourceMap; + } + + /** + * @return the rTypeResourceDirectoryMap + */ + public Map>> getRTypeResourceDirectoryMap() { + return rTypeResourceDirectoryMap; + } + +///** +// * @return the rTypeIncreaseResourceDirectoryListMap +// */ +//public Map> getRTypeIncreaseResourceDirectoryListMap() { +//return rTypeIncreaseResourceDirectoryListMap; +//} + + void addIgnoreId(String name) { + ignoreIdSet.add(name); + } + + /** + * @return the ignoreIdSet + */ + public Set getIgnoreIdSet() { + return ignoreIdSet; + } + + private static class ResourceIdEnumerator { + + private int currentId = 0; + + ResourceIdEnumerator() { + } + + ResourceIdEnumerator(int typeId) { + this.currentId = 0x7f000000 + 0x10000 * typeId + -1; + } + + int previous() { + return --currentId; + } + + int next() { + return ++currentId; + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptUtil.java new file mode 100644 index 00000000..cf61680a --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/AaptUtil.java @@ -0,0 +1,489 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +public final class AaptUtil { + + private static final String ID_DEFINITION_PREFIX = "@+id/"; + private static final String ITEM_TAG = "item"; + + private static final XPathExpression ANDROID_ID_USAGE = createExpression("//@*[starts-with(., '@') and " + "not(starts-with(., '@+')) and " + "not(starts-with(., '@android:')) and " + "not(starts-with(., '@null'))]"); + + private static final XPathExpression ANDROID_ID_DEFINITION = createExpression("//@*[starts-with(., '@+') and " + "not(starts-with(., '@+android:id'))]"); + + private static final Map RESOURCE_TYPES = getResourceTypes(); + private static final List IGNORED_TAGS = Arrays.asList("eat-comment", "skip"); + + private static XPathExpression createExpression(String expressionStr) { + try { + return XPathFactory.newInstance().newXPath().compile(expressionStr); + } catch (XPathExpressionException e) { + throw new AaptUtilException(e); + } + } + + private static Map getResourceTypes() { + Map types = new HashMap(); + for (RDotTxtEntry.RType rType : RDotTxtEntry.RType.values()) { + types.put(rType.toString(), rType); + } + types.put("string-array", RDotTxtEntry.RType.ARRAY); + types.put("integer-array", RDotTxtEntry.RType.ARRAY); + types.put("declare-styleable", RDotTxtEntry.RType.STYLEABLE); + return types; + } + + public static AaptResourceCollector collectResource(List resourceDirectoryList) { + return collectResource(resourceDirectoryList, null); + } + + public static AaptResourceCollector collectResource(List resourceDirectoryList, Map> rTypeResourceMap) { + AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap); + List references = new ArrayList(); + for (String resourceDirectory : resourceDirectoryList) { + try { + collectResources(resourceDirectory, resourceCollector); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + for (String resourceDirectory : resourceDirectoryList) { + try { + processXmlFilesForIds(resourceDirectory, references, resourceCollector); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return resourceCollector; + } + + public static void processXmlFilesForIds(String resourceDirectory, List references, AaptResourceCollector resourceCollector) throws Exception { + List xmlFullFilenameList = FileUtil.findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML); + if (xmlFullFilenameList != null) { + for (String xmlFullFilename : xmlFullFilenameList) { + File xmlFile = new File(xmlFullFilename); + String parentFullFilename = xmlFile.getParent(); + File parentFile = new File(parentFullFilename); + if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith("raw")) { + // Ignore files under values* directories and raw*. + continue; + } + processXmlFile(xmlFullFilename, references, resourceCollector); + } + } + } + + private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception { + File resourceDirectoryFile = new File(resourceDirectory); + File[] fileArray = resourceDirectoryFile.listFiles(); + if (fileArray != null) { + for (File file : fileArray) { + if (file.isDirectory()) { + String directoryName = file.getName(); + if (directoryName.startsWith("values")) { + if (!isAValuesDirectory(directoryName)) { + throw new AaptUtilException("'" + directoryName + "' is not a valid values directory."); + } + processValues(file.getAbsolutePath(), resourceCollector); + } else { + processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector); + } + } + } + } + } + + /** + * is a value directory + * + * @param directoryName + * @return boolean + */ + public static boolean isAValuesDirectory(String directoryName) { + if (directoryName == null) { + throw new NullPointerException("directoryName can not be null"); + } + return directoryName.equals("values") || directoryName.startsWith("values-"); + } + + public static void processFileNamesInDirectory(String resourceDirectory, AaptResourceCollector resourceCollector) throws IOException { + File resourceDirectoryFile = new File(resourceDirectory); + String directoryName = resourceDirectoryFile.getName(); + int dashIndex = directoryName.indexOf('-'); + if (dashIndex != -1) { + directoryName = directoryName.substring(0, dashIndex); + } + + if (!RESOURCE_TYPES.containsKey(directoryName)) { + throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory."); + } + File[] fileArray = resourceDirectoryFile.listFiles(); + if (fileArray != null) { + for (File file : fileArray) { + if (file.isHidden()) { + continue; + } + String filename = file.getName(); + int dotIndex = filename.indexOf('.'); + String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename; + + RDotTxtEntry.RType rType = RESOURCE_TYPES.get(directoryName); + resourceCollector.addIntResourceIfNotPresent(rType, resourceName); + ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath()); + resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean); + } + } + } + + public static void processValues(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception { + File resourceDirectoryFile = new File(resourceDirectory); + File[] fileArray = resourceDirectoryFile.listFiles(); + if (fileArray != null) { + for (File file : fileArray) { + if (file.isHidden()) { + continue; + } + if (!file.isFile()) { + // warning + continue; + } + processValuesFile(file.getAbsolutePath(), resourceCollector); + } + } + } + + public static void processValuesFile(String valuesFullFilename, AaptResourceCollector resourceCollector) throws Exception { + Document document = JavaXmlUtil.parse(valuesFullFilename); + String directoryName = new File(valuesFullFilename).getParentFile().getName(); + Element root = document.getDocumentElement(); + + for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) { + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + String resourceType = node.getNodeName(); + if (resourceType.equals(ITEM_TAG)) { + resourceType = node.getAttributes().getNamedItem("type").getNodeValue(); + if (resourceType.equals("id")) { + resourceCollector.addIgnoreId(node.getAttributes().getNamedItem("name").getNodeValue()); + } + } + + if (IGNORED_TAGS.contains(resourceType)) { + continue; + } + + if (!RESOURCE_TYPES.containsKey(resourceType)) { + throw new AaptUtilException("Invalid resource type '<" + resourceType + ">' in '" + valuesFullFilename + "'."); + } + + RDotTxtEntry.RType rType = RESOURCE_TYPES.get(resourceType); + String resourceValue = null; + switch (rType) { + case STRING: + case COLOR: + case DIMEN: + case DRAWABLE: + case BOOL: + case INTEGER: + resourceValue = node.getTextContent().trim(); + break; + case ARRAY://has sub item + case PLURALS://has sub item + case STYLE://has sub item + case STYLEABLE://has sub item + resourceValue = subNodeToString(node); + break; + case FRACTION://no sub item + resourceValue = nodeToString(node, true); + break; + case ATTR://no sub item + resourceValue = nodeToString(node, true); + break; + } + try { + addToResourceCollector(resourceCollector, new ResourceDirectory(directoryName, valuesFullFilename), node, rType, resourceValue); + } catch (Exception e) { + throw new AaptUtilException(e.getMessage() + ",Process file error:" + valuesFullFilename, e); + } + } + } + + public static void processXmlFile(String xmlFullFilename, List references, AaptResourceCollector resourceCollector) throws IOException, XPathExpressionException { + Document document = JavaXmlUtil.parse(xmlFullFilename); + NodeList nodesWithIds = (NodeList) ANDROID_ID_DEFINITION.evaluate(document, XPathConstants.NODESET); + for (int i = 0; i < nodesWithIds.getLength(); i++) { + String resourceName = nodesWithIds.item(i).getNodeValue(); + if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) { + throw new AaptUtilException("Invalid definition of a resource: '" + resourceName + "'"); + } + + resourceCollector.addIntResourceIfNotPresent(RDotTxtEntry.RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length())); + } + + NodeList nodesUsingIds = (NodeList) ANDROID_ID_USAGE.evaluate(document, XPathConstants.NODESET); + for (int i = 0; i < nodesUsingIds.getLength(); i++) { + String resourceName = nodesUsingIds.item(i).getNodeValue(); + int slashPosition = resourceName.indexOf('/'); + if (slashPosition < 0) { + continue; + } + String rawRType = resourceName.substring(1, slashPosition); + String name = resourceName.substring(slashPosition + 1); + + if (name.startsWith("android:")) { + continue; + } + if (!RESOURCE_TYPES.containsKey(rawRType)) { + throw new AaptUtilException("Invalid reference '" + resourceName + "' in '" + xmlFullFilename + "'"); + } + RDotTxtEntry.RType rType = RESOURCE_TYPES.get(rawRType); + +//if(!resourceCollector.isContainResource(rType, IdType.INT, sanitizeName(resourceCollector, name))){ +//throw new AaptUtilException("Not found reference '" + resourceName + "' in '" + xmlFullFilename + "'"); +//} + references.add(new FakeRDotTxtEntry(RDotTxtEntry.IdType.INT, rType, sanitizeName(rType, resourceCollector, name))); + } + } + + private static void addToResourceCollector(AaptResourceCollector resourceCollector, ResourceDirectory resourceDirectory, Node node, RDotTxtEntry.RType rType, String resourceValue) { + String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node)); + resourceCollector.addRTypeResourceName(rType, resourceName, resourceValue, resourceDirectory); + if (rType.equals(RDotTxtEntry.RType.STYLEABLE)) { + + int count = 0; + for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) { + if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals("attr")) { + continue; + } + + String rawAttrName = extractNameAttribute(attrNode); + String attrName = sanitizeName(rType, resourceCollector, rawAttrName); + resourceCollector.addResource(RDotTxtEntry.RType.STYLEABLE, RDotTxtEntry.IdType.INT, String.format("%s_%s", resourceName, attrName), Integer.toString(count++)); + + if (!rawAttrName.startsWith("android:")) { + resourceCollector.addIntResourceIfNotPresent(RDotTxtEntry.RType.ATTR, rawAttrName); + resourceCollector.addRTypeResourceName(RDotTxtEntry.RType.ATTR, rawAttrName, nodeToString(attrNode, true), resourceDirectory); + } + } + + resourceCollector.addIntArrayResourceIfNotPresent(rType, resourceName, count); + } else { + resourceCollector.addIntResourceIfNotPresent(rType, resourceName); + } + } + + private static String sanitizeName(RDotTxtEntry.RType rType, AaptResourceCollector resourceCollector, String rawName) { + String sanitizeName = rawName.replaceAll("[.:]", "_"); + resourceCollector.putSanitizeName(rType, sanitizeName, rawName); + return sanitizeName; + } + + private static String extractNameAttribute(Node node) { + return node.getAttributes().getNamedItem("name").getNodeValue(); + } + + /** + * merge package r type resource map + * + * @param packageRTypeResourceMapList + * @return Map>> + */ + public static Map>> mergePackageRTypeResourceMap(List packageRTypeResourceMapList) { + Map>> packageRTypeResourceMergeMap = new HashMap>>(); + Map aaptResourceCollectorMap = new HashMap(); + for (PackageRTypeResourceMap packageRTypeResourceMap : packageRTypeResourceMapList) { + String packageName = packageRTypeResourceMap.packageName; + Map> rTypeResourceMap = packageRTypeResourceMap.rTypeResourceMap; + AaptResourceCollector aaptResourceCollector = null; + if (aaptResourceCollectorMap.containsKey(packageName)) { + aaptResourceCollector = aaptResourceCollectorMap.get(packageName); + } else { + aaptResourceCollector = new AaptResourceCollector(); + aaptResourceCollectorMap.put(packageName, aaptResourceCollector); + } + Iterator>> iterator = rTypeResourceMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + RDotTxtEntry.RType rType = entry.getKey(); + Set rDotTxtEntrySet = entry.getValue(); + for (RDotTxtEntry rDotTxtEntry : rDotTxtEntrySet) { + if (rDotTxtEntry.idType.equals(RDotTxtEntry.IdType.INT)) { + aaptResourceCollector.addIntResourceIfNotPresent(rType, rDotTxtEntry.name); + } else if (rDotTxtEntry.idType.equals(RDotTxtEntry.IdType.INT_ARRAY)) { + aaptResourceCollector.addResource(rType, rDotTxtEntry.idType, rDotTxtEntry.name, rDotTxtEntry.idValue.trim()); + } + } + } + } + Iterator> iterator = aaptResourceCollectorMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + packageRTypeResourceMergeMap.put(entry.getKey(), entry.getValue().getRTypeResourceMap()); + } + return packageRTypeResourceMergeMap; + } + + /** + * write R.java + * + * @param outputDirectory + * @param packageName + * @param rTypeResourceMap + * @param isFinal + */ + public static void writeRJava(String outputDirectory, String packageName, Map> rTypeResourceMap, boolean isFinal) { + String outputFullFilename = new File(outputDirectory).getAbsolutePath() + Constant.Symbol.SLASH_LEFT + (packageName.replace(Constant.Symbol.DOT, Constant.Symbol.SLASH_LEFT) + Constant.Symbol.SLASH_LEFT + "R" + Constant.Symbol.DOT + Constant.File.JAVA); + FileUtil.createFile(outputFullFilename); + PrintWriter writer = null; + try { + writer = new PrintWriter(new FileOutputStream(outputFullFilename)); + writer.format("package %s;\n\n", packageName); + writer.println("public final class R {\n"); + for (RDotTxtEntry.RType rType : rTypeResourceMap.keySet()) { + // Now start the block for the new type. + writer.format(" public static final class %s {\n", rType.toString()); + for (RDotTxtEntry rDotTxtEntry : rTypeResourceMap.get(rType)) { + // Write out the resource. + // Write as an int. + writer.format(" public static%s%s %s=%s;\n", isFinal ? " final " : " ", rDotTxtEntry.idType, rDotTxtEntry.name, rDotTxtEntry.idValue.trim()); + } + writer.println(" }\n"); + } + // Close the class definition. + writer.println("}"); + } catch (Exception e) { + throw new AaptUtilException(e); + } finally { + if (writer != null) { + writer.flush(); + writer.close(); + } + } + } + + /** + * write R.java + * + * @param outputDirectory + * @param packageRTypeResourceMap + * @param isFinal + * @throws IOException + */ + public static void writeRJava(String outputDirectory, Map>> packageRTypeResourceMap, boolean isFinal) { + for (String packageName : packageRTypeResourceMap.keySet()) { + Map> rTypeResourceMap = packageRTypeResourceMap.get(packageName); + writeRJava(outputDirectory, packageName, rTypeResourceMap, isFinal); + } + } + + private static String subNodeToString(Node node) { + StringBuilder stringBuilder = new StringBuilder(); + if (node != null) { + NodeList nodeList = node.getChildNodes(); + stringBuilder.append(nodeToString(node, false)); + stringBuilder.append(StringUtil.CRLF_STRING); + int nodeListLength = nodeList.getLength(); + for (int i = 0; i < nodeListLength; i++) { + Node childNode = nodeList.item(i); + if (childNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + stringBuilder.append(nodeToString(childNode, true)); + stringBuilder.append(StringUtil.CRLF_STRING); + } + if (stringBuilder.length() > StringUtil.CRLF_STRING.length()) { + stringBuilder.delete(stringBuilder.length() - StringUtil.CRLF_STRING.length(), stringBuilder.length()); + } + } + return stringBuilder.toString(); + } + + private static String nodeToString(Node node, boolean isNoChild) { + StringBuilder stringBuilder = new StringBuilder(); + if (node != null) { + stringBuilder.append(node.getNodeName()); + NamedNodeMap namedNodeMap = node.getAttributes(); + stringBuilder.append(Constant.Symbol.MIDDLE_BRACKET_LEFT); + int namedNodeMapLength = namedNodeMap.getLength(); + for (int j = 0; j < namedNodeMapLength; j++) { + Node attributeNode = namedNodeMap.item(j); + stringBuilder.append(Constant.Symbol.AT + attributeNode.getNodeName() + Constant.Symbol.EQUAL + attributeNode.getNodeValue()); + if (j < namedNodeMapLength - 1) { + stringBuilder.append(Constant.Symbol.COMMA); + } + } + stringBuilder.append(Constant.Symbol.MIDDLE_BRACKET_RIGHT); + String value = StringUtil.nullToBlank(isNoChild ? node.getTextContent() : node.getNodeValue()).trim(); + if (StringUtil.isNotBlank(value)) { + stringBuilder.append(Constant.Symbol.EQUAL + value); + } + } + return stringBuilder.toString(); + } + + public static class PackageRTypeResourceMap { + private String packageName = null; + private Map> rTypeResourceMap = null; + + public PackageRTypeResourceMap(String packageName, Map> rTypeResourceMap) { + this.packageName = packageName; + this.rTypeResourceMap = rTypeResourceMap; + } + } + + public static class AaptUtilException extends RuntimeException { + private static final long serialVersionUID = 1702278793911780809L; + + public AaptUtilException(String message) { + super(message); + } + + public AaptUtilException(Throwable cause) { + super(cause); + } + + public AaptUtilException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Constant.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Constant.java new file mode 100644 index 00000000..a3a11154 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Constant.java @@ -0,0 +1,302 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +public interface Constant { + + interface Base { + String EXCEPTION = "exception"; + } + + interface Symbol { + /** + * dot "." + */ + String DOT = "."; + char DOT_CHAR = '.'; + /** + * comma "," + */ + String COMMA = ","; + /** + * colon ":" + */ + String COLON = ":"; + /** + * semicolon ";" + */ + String SEMICOLON = ";"; + /** + * equal "=" + */ + String EQUAL = "="; + /** + * and "&" + */ + String AND = "&"; + /** + * question mark "?" + */ + String QUESTION_MARK = "?"; + /** + * wildcard "*" + */ + String WILDCARD = "*"; + /** + * underline "_" + */ + String UNDERLINE = "_"; + /** + * at "@" + */ + String AT = "@"; + /** + * minus "-" + */ + String MINUS = "-"; + /** + * logic and "&&" + */ + String LOGIC_AND = "&&"; + /** + * logic or "||" + */ + String LOGIC_OR = "||"; + /** + * brackets begin "(" + */ + String BRACKET_LEFT = "("; + /** + * brackets end ")" + */ + String BRACKET_RIGHT = ")"; + /** + * middle bracket left "[" + */ + String MIDDLE_BRACKET_LEFT = "["; + /** + * middle bracket right "]" + */ + String MIDDLE_BRACKET_RIGHT = "]"; + /** + * big bracket "{" + */ + String BIG_BRACKET_LEFT = "{"; + /** + * big bracket "}" + */ + String BIG_BRACKET_RIGHT = "}"; + /** + * slash "/" + */ + String SLASH_LEFT = "/"; + /** + * slash "\" + */ + String SLASH_RIGHT = "\\"; + /** + * xor or regex begin "^" + */ + String XOR = "^"; + /** + * dollar or regex end "$" + */ + String DOLLAR = "$"; + /** + * single quotes "'" + */ + String SINGLE_QUOTES = "'"; + /** + * double quotes "\"" + */ + String DOUBLE_QUOTES = "\""; + } + + interface Encoding { + /** + * encoding + */ + String ISO88591 = "ISO-8859-1"; + String GB2312 = "GB2312"; + String GBK = "GBK"; + String UTF8 = "UTF-8"; + } + + interface Timezone { + String ASIA_SHANGHAI = "Asia/Shanghai"; + } + + interface Http { + + interface RequestMethod { + /** + * for request method + */ + String PUT = "PUT"; + String DELETE = "DELETE"; + String GET = "GET"; + String POST = "POST"; + String HEAD = "HEAD"; + String OPTIONS = "OPTIONS"; + String TRACE = "TRACE"; + } + + interface HeaderKey { + /** + * for request,response header + */ + String CONTENT_TYPE = "Content-Type"; + String CONTENT_DISPOSITION = "Content-Disposition"; + String ACCEPT_CHARSET = "Accept-Charset"; + String CONTENT_ENCODING = "Content-Encoding"; + } + + interface ContentType { + /** + * for request,response content type + */ + String TEXT_PLAIN = "text/plain"; + String APPLICATION_X_DOWNLOAD = "application/x-download"; + String APPLICATION_ANDROID_PACKAGE = "application/vnd.android.package-archive"; + String MULTIPART_FORM_DATA = "multipart/form-data"; + String APPLICATION_OCTET_STREAM = "application/octet-stream"; + String BINARY_OCTET_STREAM = "binary/octet-stream"; + String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; + } + + interface StatusCode { + + int CONTINUE = 100; + int SWITCHING_PROTOCOLS = 101; + int PROCESSING = 102; + + int OK = 200; + int CREATED = 201; + int ACCEPTED = 202; + int NON_AUTHORITATIVE_INFORMATION = 203; + int NO_CONTENT = 204; + int RESET_CONTENT = 205; + int PARTIAL_CONTENT = 206; + int MULTI_STATUS = 207; + + int MULTIPLE_CHOICES = 300; + int MOVED_PERMANENTLY = 301; + int FOUND = 302; + int SEE_OTHER = 303; + int NOT_MODIFIED = 304; + int USE_PROXY = 305; + int SWITCH_PROXY = 306; + int TEMPORARY_REDIRECT = 307; + + int BAD_REQUEST = 400; + int UNAUTHORIZED = 401; + int PAYMENT_REQUIRED = 402; + int FORBIDDEN = 403; + int NOT_FOUND = 404; + int METHOD_NOT_ALLOWED = 405; + int NOT_ACCEPTABLE = 406; + int REQUEST_TIMEOUT = 408; + int CONFLICT = 409; + int GONE = 410; + int LENGTH_REQUIRED = 411; + int PRECONDITION_FAILED = 412; + int REQUEST_URI_TOO_LONG = 414; + int EXPECTATION_FAILED = 417; + int TOO_MANY_CONNECTIONS = 421; + int UNPROCESSABLE_ENTITY = 422; + int LOCKED = 423; + int FAILED_DEPENDENCY = 424; + int UNORDERED_COLLECTION = 425; + int UPGRADE_REQUIRED = 426; + int RETRY_WITH = 449; + + int INTERNAL_SERVER_ERROR = 500; + int NOT_IMPLEMENTED = 501; + int BAD_GATEWAY = 502; + int SERVICE_UNAVAILABLE = 503; + int GATEWAY_TIMEOUT = 504; + int HTTP_VERSION_NOT_SUPPORTED = 505; + int VARIANT_ALSO_NEGOTIATES = 506; + int INSUFFICIENT_STORAGE = 507; + int LOOP_DETECTED = 508; + int BANDWIDTH_LIMIT_EXCEEDED = 509; + int NOT_EXTENDED = 510; + int UNPARSEABLE_RESPONSE_HEADERS = 600; + } + } + + interface RequestScope { + String SESSION = "session"; + } + + interface RequestParameter { + String RETURN_URL = "returnUrl"; + } + + interface Database { + String COLUMN_NAME_TOTAL = "TOTAL"; + + interface MySql { + /** + * pagination + */ + String PAGINATION = "LIMIT"; + } + } + + interface Capacity { + /** + * bytes per kilobytes + */ + int BYTES_PER_KB = 1024; + + /** + * bytes per millionbytes + */ + int BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB; + } + + interface Method { + String PREFIX_SET = "set"; + String PREFIX_GET = "get"; + String PREFIX_IS = "is"; + String GET_CLASS = "getClass"; + } + + interface File { + String CLASS = "class"; + String JPEG = "jpeg"; + String JPG = "jpg"; + String GIF = "gif"; + String JAR = "jar"; + String JAVA = "java"; + String EXE = "exe"; + String DEX = "dex"; + String AIDL = "aidl"; + String SO = "so"; + String XML = "xml"; + String CSV = "csv"; + String TXT = "txt"; + String APK = "apk"; + } + + interface Protocol { + String FILE = "file://"; + String HTTP = "http://"; + String FTP = "ftp://"; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/DefaultFileCopyProcessor.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/DefaultFileCopyProcessor.java new file mode 100644 index 00000000..6a95350a --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/DefaultFileCopyProcessor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +public class DefaultFileCopyProcessor implements FileUtil.FileCopyProcessor { + + /** + * copyFileToFileProcess + * + * @param from,maybe directory + * @param to,maybe directory + * @param isFile,maybe directory or file + * @return boolean, if true keep going copy,only active in directory so far + */ + public boolean copyFileToFileProcess(final String from, final String to, final boolean isFile) { + try { + if (isFile) { + String fromFile = new File(from).getAbsolutePath(); + String toFile = new File(to).getAbsolutePath(); + if (fromFile.equals(toFile)) { + toFile = toFile + "_copy"; + } + FileUtil.createFile(toFile); + InputStream inputStream = new FileInputStream(fromFile); + OutputStream outputStream = new FileOutputStream(toFile); + try { + byte[] buffer = new byte[Constant.Capacity.BYTES_PER_KB]; + int length = -1; + while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { + outputStream.write(buffer, 0, length); + outputStream.flush(); + } + } finally { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } + } else { + FileUtil.createDirectory(to); + } + } catch (Exception e) { + throw new FileCopyException(e); + } + return true; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FakeRDotTxtEntry.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FakeRDotTxtEntry.java new file mode 100644 index 00000000..7c018630 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FakeRDotTxtEntry.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +/** + * An {@link RDotTxtEntry} with fake {@link #idValue}, useful for comparing two resource entries for + * equality, since {@link RDotTxtEntry#compareTo(RDotTxtEntry)} ignores the id value. + */ +public class FakeRDotTxtEntry extends RDotTxtEntry { + + private static final String FAKE_ID = "0x00000000"; + + public FakeRDotTxtEntry(IdType idType, RType type, String name) { + super(idType, type, name, FAKE_ID); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileCopyException.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileCopyException.java new file mode 100644 index 00000000..2eb5f5c0 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileCopyException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +public class FileCopyException extends RuntimeException { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -6670157031514003361L; + + /** + * @param message + */ + public FileCopyException(String message) { + super(message); + } + + /** + * @param cause + */ + public FileCopyException(Throwable cause) { + super(cause); + } + + /** + * @param message + * @param cause + */ + public FileCopyException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileUtil.java new file mode 100644 index 00000000..0055e9d8 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/FileUtil.java @@ -0,0 +1,1286 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public final class FileUtil { + + private static final FileCopyProcessor DEFAULT_FILE_COPY_PROCESSOR = new DefaultFileCopyProcessor(); + + private FileUtil() { + } + + /** + * is file exist,include directory or file + * + * @param path directory or file + * @return boolean + */ + public static boolean isExist(String path) { + File file = new File(path); + return file.exists(); + } + + /** + * is has file from directory + * + * @param directory + * @param fileSuffix + * @return boolean + */ + public static boolean isHasFile(String directory, String fileSuffix) { + boolean result = false; + File directoryFile = new File(directory); + Queue queue = new ConcurrentLinkedQueue(); + queue.add(directoryFile); + while (!queue.isEmpty()) { + File file = queue.poll(); + if (file.isDirectory()) { + File[] fileArray = file.listFiles(); + if (fileArray != null) { + queue.addAll(Arrays.asList(fileArray)); + } + } else if (file.isFile()) { + if (file.getName().toLowerCase().endsWith(fileSuffix.toLowerCase())) { + result = true; + break; + } + } + } + return result; + } + + /** + * create directory + * + * @param directoryPath + */ + public static void createDirectory(final String directoryPath) { + File file = new File(directoryPath); + if (!file.exists()) { + file.setReadable(true, false); + file.setWritable(true, true); + file.mkdirs(); + } + } + + /** + * create file,full filename,signle empty file. + * + * @param fullFilename + * @return boolean + */ + public static boolean createFile(final String fullFilename) { + boolean result = false; + File file = new File(fullFilename); + createDirectory(file.getParent()); + try { + file.setReadable(true, false); + file.setWritable(true, true); + result = file.createNewFile(); + } catch (Exception e) { + throw new FileUtilException(e); + } + return result; + } + + /** + * delete all file + * + * @param directory + */ + public static void deleteAllFile(String directory) { + List fileList = new ArrayList(); + File directoryFile = new File(directory); + Queue queue = new ConcurrentLinkedQueue(); + queue.add(directoryFile); + while (!queue.isEmpty()) { + File file = queue.poll(); + if (file.isDirectory()) { + File[] fileArray = file.listFiles(); + if (fileArray != null) { + queue.addAll(Arrays.asList(fileArray)); + } + } + fileList.add(file); + } + for (int i = fileList.size() - 1; i >= 0; i--) { + fileList.get(i).delete(); + } + } + + /** + * copy file,default path to path + * + * @param from + * @param to + */ + public static void copyFile(final String from, final String to) { + copyFile(from, to, FileCopyType.PATH_TO_PATH, DEFAULT_FILE_COPY_PROCESSOR); + } + + /** + * copy file + * + * @param from + * @param to + * @param fileCopyType + */ + public static void copyFile(final String from, final String to, final FileCopyType fileCopyType) { + copyFile(from, to, fileCopyType, DEFAULT_FILE_COPY_PROCESSOR); + } + + /** + * copy file + * + * @param from + * @param to + * @param fileCopyType + * @param fileCopyProcessor + */ + public static void copyFile(final String from, final String to, final FileCopyType fileCopyType, FileCopyProcessor fileCopyProcessor) { + switch (fileCopyType) { + case FILE_TO_PATH: + copyFileToPath(from, to, fileCopyProcessor); + break; + case FILE_TO_FILE: + copyFileToFile(from, to, fileCopyProcessor); + break; + case PATH_TO_PATH: + default: + copyPathToPath(from, to, fileCopyProcessor); + break; + } + } + + /** + * copy path to path,copy process include directory copy + * + * @param fromPath + * @param toPath + * @param fileCopyProcessor + */ + public static void copyPathToPath(final String fromPath, final String toPath, FileCopyProcessor fileCopyProcessor) { + File fromDirectoryFile = new File(fromPath); + File toDirectoryFile = new File(toPath); + String fromDirectoryPath = fromDirectoryFile.getAbsolutePath(); + String toDirectoryPath = toDirectoryFile.getAbsolutePath(); + if (fromDirectoryPath.equals(toDirectoryPath)) { + toDirectoryPath = toDirectoryPath + "_copy"; + } + Queue queue = new ConcurrentLinkedQueue(); + queue.add(fromDirectoryFile); + while (!queue.isEmpty()) { + File file = queue.poll(); + String fromFilePath = file.getAbsolutePath(); + String toFilePath = toDirectoryPath + fromFilePath.substring(fromDirectoryPath.length()); + if (file.isDirectory()) { + boolean result = true; + if (fileCopyProcessor != null) { + result = fileCopyProcessor.copyFileToFileProcess(fromFilePath, toFilePath, false); + } + if (result) { + File[] fileArray = file.listFiles(); + if (fileArray != null) { + queue.addAll(Arrays.asList(fileArray)); + } + } + } else if (file.isFile()) { + if (fileCopyProcessor != null) { + fileCopyProcessor.copyFileToFileProcess(fromFilePath, toFilePath, true); + } + } + } + } + + /** + * @param fromFile + * @param toPath + * @param fileCopyProcessor + */ + private static void copyFileToPath(final String fromFile, final String toPath, final FileCopyProcessor fileCopyProcessor) { + File from = new File(fromFile); + File to = new File(toPath); + if (from.exists() && from.isFile()) { + createDirectory(toPath); + String tempFromFile = from.getAbsolutePath(); + String tempToFile = to.getAbsolutePath() + File.separator + from.getName(); + copyFileToFile(tempFromFile, tempToFile, fileCopyProcessor); + } + } + + /** + * unzip + * + * @param zipFullFilename + * @param outputDirectory + * @return List + */ + public static List unzip(String zipFullFilename, String outputDirectory) { + return unzip(zipFullFilename, outputDirectory, null); + } + + /** + * unzip + * + * @param zipFullFilename + * @param outputDirectory + * @param zipEntryNameList,if it is null or empty,will unzip all + * @return List + */ + public static List unzip(String zipFullFilename, String outputDirectory, List zipEntryNameList) { + if (outputDirectory == null) { + throw new NullPointerException("out put directory can not be null."); + } + List storeFileList = null; + ZipFile zipFile = null; + try { + storeFileList = new ArrayList(); + zipFile = new ZipFile(zipFullFilename); + String outputDirectoryAbsolutePath = new File(outputDirectory).getAbsolutePath(); + Enumeration enumeration = zipFile.entries(); + while (enumeration.hasMoreElements()) { + ZipEntry zipEntry = enumeration.nextElement(); + String zipEntryName = zipEntry.getName(); + boolean contains = false; + if (zipEntryNameList == null || zipEntryNameList.isEmpty()) { + contains = true; + } else { + if (zipEntryNameList.contains(zipEntryName)) { + contains = true; + } + } + if (contains) { + InputStream inputStream = zipFile.getInputStream(zipEntry); + String outputFullFilename = outputDirectoryAbsolutePath + Constant.Symbol.SLASH_LEFT + zipEntryName; + if (zipEntry.isDirectory()) { + createDirectory(outputFullFilename); + } else { + createFile(outputFullFilename); + OutputStream outputStream = new FileOutputStream(outputFullFilename); + try { + byte[] buffer = new byte[Constant.Capacity.BYTES_PER_KB]; + int length = -1; + while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { + outputStream.write(buffer, 0, length); + outputStream.flush(); + } + } finally { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } + storeFileList.add(outputFullFilename); + } + } + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + try { + if (zipFile != null) { + zipFile.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return storeFileList; + } + + /** + * zip + * + * @param outputZipFullFilename + * @param directory + */ + public static void zip(String outputZipFullFilename, String directory) { + zip(outputZipFullFilename, directory, StringUtil.BLANK); + } + + /** + * zip + * + * @param outputZipFullFilename + * @param directory + * @param fileSuffix + */ + public static void zip(String outputZipFullFilename, String directory, String fileSuffix) { + List classFileList = FileUtil.findMatchFile(directory, fileSuffix); + if (classFileList != null && !classFileList.isEmpty()) { + List zipEntryPathList = new ArrayList(); + int classOutputFullFilenameLength = new File(directory).getAbsolutePath().length() + 1; + for (String classFile : classFileList) { + String zipEntryName = classFile.substring(classOutputFullFilenameLength, classFile.length()); + zipEntryName = zipEntryName.replace(Constant.Symbol.SLASH_RIGHT, Constant.Symbol.SLASH_LEFT); + zipEntryPathList.add(new ZipEntryPath(classFile, new ZipEntry(zipEntryName), true)); + } + zip(outputZipFullFilename, zipEntryPathList); + } + } + + /** + * zip + * + * @param outputZipFullFilename + * @param zipEntryPathList + */ + public static void zip(String outputZipFullFilename, List zipEntryPathList) { + zip(outputZipFullFilename, null, zipEntryPathList); + } + + /** + * zip + * + * @param outputZipFullFilename + * @param inputZipFullFilename,can null,the entry will not from the input file + * @param zipEntryPathList + */ + public static void zip(String outputZipFullFilename, String inputZipFullFilename, List zipEntryPathList) { + zip(outputZipFullFilename, inputZipFullFilename, zipEntryPathList, null); + } + + /** + * zip + * + * @param outputZipFullFilename + * @param inputZipFullFilename,can null,the entry will not from the input file + * @param zipProcessor + */ + public static void zip(String outputZipFullFilename, String inputZipFullFilename, ZipProcessor zipProcessor) { + zip(outputZipFullFilename, inputZipFullFilename, null, zipProcessor); + } + + /** + * zip + * + * @param outputZipFullFilename + * @param inputZipFullFilename,can null,the entry will not from the input file + * @param zipEntryPathList + * @param zipProcessor + */ + public static void zip(String outputZipFullFilename, String inputZipFullFilename, List zipEntryPathList, ZipProcessor zipProcessor) { + ZipOutputStream zipOutputStream = null; + ZipFile zipFile = null; + Map zipEntryPathMap = new HashMap(); + List needToAddEntryNameList = new CopyOnWriteArrayList(); + if (zipEntryPathList != null) { + for (ZipEntryPath zipEntryPath : zipEntryPathList) { + zipEntryPathMap.put(zipEntryPath.zipEntry.getName(), zipEntryPath); + needToAddEntryNameList.add(zipEntryPath.zipEntry.getName()); + } + } + try { + createFile(outputZipFullFilename); + zipOutputStream = new ZipOutputStream(new FileOutputStream(outputZipFullFilename)); + if (inputZipFullFilename != null) { + zipFile = new ZipFile(inputZipFullFilename); + Enumeration enumeration = zipFile.entries(); + while (enumeration.hasMoreElements()) { + ZipEntry zipEntry = enumeration.nextElement(); + String zipEntryName = zipEntry.getName(); + InputStream inputStream = null; + if (zipEntryPathMap.containsKey(zipEntryName)) { + ZipEntryPath zipEntryPath = zipEntryPathMap.get(zipEntryName); + needToAddEntryNameList.remove(zipEntryName); + if (zipEntryPath.replace) { + zipEntry = zipEntryPath.zipEntry; + inputStream = new FileInputStream(zipEntryPath.fullFilename); + } + } + if (inputStream == null) { + inputStream = zipFile.getInputStream(zipEntry); + if (zipProcessor != null) { + inputStream = zipProcessor.zipEntryProcess(zipEntryName, inputStream); + } + } + ZipEntry newZipEntry = new ZipEntry(zipEntryName); + addZipEntry(zipOutputStream, newZipEntry, inputStream); + } + } + for (String zipEntryName : needToAddEntryNameList) { + ZipEntryPath zipEntryPath = zipEntryPathMap.get(zipEntryName); + ZipEntry zipEntry = zipEntryPath.zipEntry; + InputStream inputStream = new FileInputStream(zipEntryPath.fullFilename); + if (zipProcessor != null) { + inputStream = zipProcessor.zipEntryProcess(zipEntry.getName(), inputStream); + } + addZipEntry(zipOutputStream, zipEntry, inputStream); + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + try { + if (zipOutputStream != null) { + zipOutputStream.finish(); + zipOutputStream.flush(); + zipOutputStream.close(); + } + if (zipFile != null) { + zipFile.close(); + } + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + + /** + * merge zip file + * + * @param zipOutputFullFilename + * @param mergeZipFullFilenameList + */ + public static void mergeZip(String zipOutputFullFilename, List mergeZipFullFilenameList) { + FileUtil.createFile(zipOutputFullFilename); + ZipOutputStream zipOutputStream = null; + try { + zipOutputStream = new ZipOutputStream(new FileOutputStream(zipOutputFullFilename)); + if (mergeZipFullFilenameList != null) { + for (String zipFullFilename : mergeZipFullFilenameList) { + if (isExist(zipFullFilename)) { + ZipFile zipFile = new ZipFile(zipFullFilename); + Enumeration enumeration = zipFile.entries(); + while (enumeration.hasMoreElements()) { + ZipEntry zipEntry = enumeration.nextElement(); + InputStream inputStream = zipFile.getInputStream(zipEntry); + addZipEntry(zipOutputStream, zipEntry, inputStream); + } + zipFile.close(); + } + } + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + try { + if (zipOutputStream != null) { + zipOutputStream.close(); + } + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + + /** + * add zip entry + * + * @param zipOutputStream + * @param zipEntry + * @param inputStream + * @throws Exception + */ + public static void addZipEntry(ZipOutputStream zipOutputStream, ZipEntry zipEntry, InputStream inputStream) throws Exception { + try { + zipOutputStream.putNextEntry(zipEntry); + byte[] buffer = new byte[Constant.Capacity.BYTES_PER_KB]; + int length = -1; + while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { + zipOutputStream.write(buffer, 0, length); + zipOutputStream.flush(); + } + } catch (ZipException e) { + // do nothing + } finally { + if (inputStream != null) { + inputStream.close(); + } + zipOutputStream.closeEntry(); + } + } + + /** + * read file + * + * @param fullFilename + * @return byte[] + */ + public static byte[] readFile(String fullFilename) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + InputStream inputStream = null; + try { + inputStream = new FileInputStream(fullFilename); + copyStream(inputStream, byteArrayOutputStream); + } catch (FileNotFoundException e) { + throw new FileUtilException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + if (byteArrayOutputStream != null) { + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + return byteArrayOutputStream.toByteArray(); + } + + /** + * write file + * + * @param outputFullFilename + * @param byteArray + */ + public static void writeFile(String outputFullFilename, byte[] byteArray) { + InputStream inputStream = new ByteArrayInputStream(byteArray); + FileUtil.createFile(outputFullFilename); + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(outputFullFilename); + copyStream(inputStream, outputStream); + } catch (FileNotFoundException e) { + throw new FileUtilException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + } + + /** + * copy stream , from input to output,it don't close + * + * @param inputStream + * @param outputStream + */ + public static void copyStream(InputStream inputStream, OutputStream outputStream) { + if (inputStream != null && outputStream != null) { + try { + int length = -1; + byte[] buffer = new byte[Constant.Capacity.BYTES_PER_MB]; + while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { + outputStream.write(buffer, 0, length); + outputStream.flush(); + } + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + + /** + * merge file + * + * @param outputFullFilename + * @param fullFilenameList + */ + public static void mergeFile(String outputFullFilename, List fullFilenameList) { + if (fullFilenameList != null && outputFullFilename != null) { + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(outputFullFilename); + for (String fullFilename : fullFilenameList) { + InputStream inputStream = null; + try { + inputStream = new FileInputStream(fullFilename); + copyStream(inputStream, outputStream); + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + } + } + + /** + * find match file directory + * + * @param sourceDirectory + * @param fileSuffix + * @return List + */ + public static List findMatchFileDirectory(String sourceDirectory, String fileSuffix) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, null, false, true); + } + + /** + * find match file directory + * + * @param sourceDirectory + * @param fileSuffix + * @param includeHidden + * @return List + */ + public static List findMatchFileDirectory(String sourceDirectory, String fileSuffix, boolean includeHidden) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, null, false, includeHidden); + } + + /** + * find match file directory and append some string to rear + * + * @param sourceDirectory + * @param fileSuffix + * @param somethingAppendToRear + * @return List + */ + public static List findMatchFileDirectory(String sourceDirectory, String fileSuffix, String somethingAppendToRear) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, false, true); + } + + /** + * find match file directory and append some string to rear + * + * @param sourceDirectory + * @param fileSuffix + * @param somethingAppendToRear + * @param includeHidden + * @return List + */ + public static List findMatchFileDirectory(String sourceDirectory, String fileSuffix, String somethingAppendToRear, boolean includeHidden) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, false, includeHidden); + } + + /** + * find match file + * + * @param sourceDirectory + * @param fileSuffix + * @return List + */ + public static List findMatchFile(String sourceDirectory, String fileSuffix) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, null, true, true); + } + + /** + * find match file + * + * @param sourceDirectory + * @param fileSuffix + * @param includeHidden + * @return List + */ + public static List findMatchFile(String sourceDirectory, String fileSuffix, boolean includeHidden) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, null, true, includeHidden); + } + + /** + * find match file and append some string to rear + * + * @param sourceDirectory + * @param fileSuffix + * @param somethingAppendToRear + * @return List + */ + public static List findMatchFile(String sourceDirectory, String fileSuffix, String somethingAppendToRear) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, true, false); + } + + /** + * find match file and append some string to rear + * + * @param sourceDirectory + * @param fileSuffix + * @param somethingAppendToRear + * @param includeHidden + * @return List + */ + public static List findMatchFile(String sourceDirectory, String fileSuffix, String somethingAppendToRear, boolean includeHidden) { + return findMatchFileOrMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, true, includeHidden); + } + + /** + * find match file or match file directory + * + * @param sourceDirectory + * @param fileSuffix + * @param somethingAppendToRear + * @param isFindMatchFile + * @param includeHidden + * @return List + */ + private static List findMatchFileOrMatchFileDirectory(String sourceDirectory, String fileSuffix, String somethingAppendToRear, boolean isFindMatchFile, boolean includeHidden) { + fileSuffix = StringUtil.nullToBlank(fileSuffix); + somethingAppendToRear = StringUtil.nullToBlank(somethingAppendToRear); + List list = new ArrayList(); + File sourceDirectoryFile = new File(sourceDirectory); + Queue queue = new ConcurrentLinkedQueue(); + queue.add(sourceDirectoryFile); + while (!queue.isEmpty()) { + File file = queue.poll(); + boolean result = false; + if (!file.isHidden() || includeHidden) { + result = true; + } + if (result) { + if (file.isDirectory()) { + File[] fileArray = file.listFiles(); + if (fileArray != null) { + queue.addAll(Arrays.asList(fileArray)); + } + } else if (file.isFile()) { + if (file.getName().toLowerCase().endsWith(fileSuffix.toLowerCase())) { + if (isFindMatchFile) { + list.add(file.getAbsolutePath() + somethingAppendToRear); + } else { + String parentPath = file.getParent(); + parentPath = parentPath + somethingAppendToRear; + if (!list.contains(parentPath)) { + list.add(parentPath); + } + } + } + } + } + } + return list; + } + + /** + * get zip entry hash map + * + * @param zipFile + * @return Map + */ + private static Map getZipEntryHashMap(String zipFullFilename) { + ZipFile zipFile = null; + Map map = new HashMap(); + try { + zipFile = new ZipFile(zipFullFilename); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry zipEntry = (ZipEntry) entries.nextElement(); + if (!zipEntry.isDirectory()) { + String key = zipEntry.getName(); + String value = zipEntry.getCrc() + Constant.Symbol.DOT + zipEntry.getSize(); + map.put(key, value); + } + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + return map; + } + + /** + * differ zip + * + * @param differentOutputFullFilename + * @param oldZipFullFilename + * @param newZipFullFilename + */ + public static void differZip(String differentOutputFullFilename, String oldZipFullFilename, String newZipFullFilename) { + Map map = getZipEntryHashMap(oldZipFullFilename); + ZipFile newZipFile = null; + ZipOutputStream zipOutputStream = null; + try { + newZipFile = new ZipFile(newZipFullFilename); + Enumeration entries = newZipFile.entries(); + FileUtil.createFile(differentOutputFullFilename); + zipOutputStream = new ZipOutputStream(new FileOutputStream(differentOutputFullFilename)); + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + if (!zipEntry.isDirectory()) { + String zipEntryName = zipEntry.getName(); + String oldZipEntryHash = map.get(zipEntryName); + String newZipEntryHash = zipEntry.getCrc() + Constant.Symbol.DOT + zipEntry.getSize(); + // old zip entry hash not exist is a new zip entry,if exist + // is a modified zip entry + if (oldZipEntryHash == null || (!newZipEntryHash.equals(oldZipEntryHash))) { + System.out.println(String.format("found modified entry, key=%s(%s/%s)", new Object[]{zipEntryName, oldZipEntryHash, newZipEntryHash})); + addZipEntry(zipOutputStream, zipEntry, newZipFile.getInputStream(zipEntry)); + } + } + } + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (newZipFile != null) { + try { + newZipFile.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + if (zipOutputStream != null) { + try { + zipOutputStream.finish(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + } + + /** + * generate simple file + * + * @param templateFullFilename + * @param outputFullFilename + * @param valueMap + */ + public static void generateSimpleFile(String templateFullFilename, String outputFullFilename, Map valueMap) { + InputStream inputStream = null; + try { + inputStream = new FileInputStream(templateFullFilename); + generateSimpleFile(inputStream, outputFullFilename, valueMap); + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + } + } + + /** + * generate simple file + * + * @param templateInputStream + * @param outputFullFilename + * @param valueMap + */ + public static void generateSimpleFile(InputStream templateInputStream, String outputFullFilename, Map valueMap) { + BufferedReader bufferedReader = null; + OutputStream outputStream = null; + try { + bufferedReader = new BufferedReader(new InputStreamReader(templateInputStream, Constant.Encoding.UTF8)); + StringBuilder content = new StringBuilder(); + String line = null; + Set> entrySet = valueMap.entrySet(); + while ((line = bufferedReader.readLine()) != null) { + for (Entry entry : entrySet) { + String key = entry.getKey(); + String value = entry.getValue(); + line = line.replace(key, value); + } + content.append(line); + content.append(StringUtil.CRLF_STRING); + } + createFile(outputFullFilename); + outputStream = new FileOutputStream(outputFullFilename); + outputStream.write(content.toString().getBytes(Constant.Encoding.UTF8)); + outputStream.flush(); + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException e) { + throw new FileUtilException(e); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + } + + /** + * find file list with cache + * + * @param sourceDirectoryList + * @param cacheProperties + * @param fileSuffix suffix it will search file in source directory list + * @param somethingAppendToRear + * @param isFile if true the return list is source file else is the source directory + * @return List + */ + public static List findFileListWithCache(List sourceDirectoryList, Properties cacheProperties, String fileSuffix, String somethingAppendToRear, boolean isFile) { + return findFileListWithCache(sourceDirectoryList, cacheProperties, fileSuffix, somethingAppendToRear, isFile, null); + } + + /** + * find file list with cache + * + * @param sourceDirectoryList + * @param cacheProperties + * @param fileSuffix + * @param somethingAppendToRear + * @param isFile + * @param cacheProcessor + * @return List + */ + public static List findFileListWithCache(List sourceDirectoryList, Properties cacheProperties, String fileSuffix, String somethingAppendToRear, boolean isFile, CacheProcessor cacheProcessor) { + return findFileListWithCache(sourceDirectoryList, cacheProperties, fileSuffix, somethingAppendToRear, isFile, false, cacheProcessor); + } + + /** + * find file list with cache + * + * @param sourceDirectoryList + * @param cacheProperties + * @param fileSuffix suffix it will search file in source directory list + * @param somethingAppendToRear + * @param isFile if true the return list is source file else is the source directory + * @param includeHidden + * @return List + */ + public static List findFileListWithCache(List sourceDirectoryList, Properties cacheProperties, String fileSuffix, String somethingAppendToRear, boolean isFile, boolean includeHidden, CacheProcessor cacheProcessor) { + List sourceList = new ArrayList(); + //no cache + if (cacheProperties == null) { + if (sourceDirectoryList != null && !sourceDirectoryList.isEmpty()) { + for (String sourceDirectory : sourceDirectoryList) { + if (isFile) { + sourceList.addAll(FileUtil.findMatchFile(sourceDirectory, fileSuffix, includeHidden)); + } else { + sourceList.addAll(FileUtil.findMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, includeHidden)); + } + } + } + } else if (cacheProperties.isEmpty()) { + List fileList = new ArrayList(); + if (sourceDirectoryList != null && !sourceDirectoryList.isEmpty()) { + for (String sourceDirectory : sourceDirectoryList) { + fileList.addAll(FileUtil.findMatchFile(sourceDirectory, fileSuffix, includeHidden)); + } + } + for (String fullFilename : fileList) { + String cacheKey = fullFilename; + if (cacheProcessor != null) { + cacheKey = cacheProcessor.keyProcess(cacheKey); + } + cacheProperties.setProperty(cacheKey, Generator.md5File(fullFilename)); + } + if (isFile) { + sourceList.addAll(fileList); + } else { + if (sourceDirectoryList != null && !sourceDirectoryList.isEmpty()) { + for (String sourceDirectory : sourceDirectoryList) { + sourceList.addAll(FileUtil.findMatchFileDirectory(sourceDirectory, fileSuffix, somethingAppendToRear, includeHidden)); + } + } + } + } else { + List fileList = new ArrayList(); + if (sourceDirectoryList != null && !sourceDirectoryList.isEmpty()) { + for (String sourceDirectory : sourceDirectoryList) { + fileList.addAll(FileUtil.findMatchFile(sourceDirectory, fileSuffix, includeHidden)); + } + } + for (String fullFilename : fileList) { + String cacheKey = fullFilename; + if (cacheProcessor != null) { + cacheKey = cacheProcessor.keyProcess(cacheKey); + } + String sourceFileMd5 = Generator.md5File(fullFilename); + if (cacheProperties.containsKey(cacheKey)) { + String md5 = cacheProperties.getProperty(cacheKey); + if (!sourceFileMd5.equals(md5)) { + sourceList.add(fullFilename); + cacheProperties.setProperty(cacheKey, sourceFileMd5); + } + } else { + sourceList.add(fullFilename); + cacheProperties.setProperty(cacheKey, sourceFileMd5); + } + } + } + return sourceList; + } + + /** + * deal with file cache + * + * @param propertiesFileMappingFullFilename + * @param noCacheFileFinder + * @param noCacheFileProcessor + * @return List + */ + public static List dealWithFileCache(String propertiesFileMappingFullFilename, NoCacheFileFinder noCacheFileFinder, NoCacheFileProcessor noCacheFileProcessor) { + Properties propertiesFileMapping = getPropertiesAutoCreate(propertiesFileMappingFullFilename); + List noCacheFileList = null; + if (noCacheFileFinder == null) { + throw new NullPointerException("noCacheFileFinder can not be null."); + } + noCacheFileList = noCacheFileFinder.findNoCacheFileList(propertiesFileMapping); + boolean saveCache = false; + if (noCacheFileProcessor != null) { + saveCache = noCacheFileProcessor.process(noCacheFileList); + } + if (saveCache) { + saveProperties(propertiesFileMapping, propertiesFileMappingFullFilename); + } + return noCacheFileList; + } + + /** + * get properties will auto create + * + * @param propertiesFullFilename + * @return Properties + */ + public static Properties getPropertiesAutoCreate(String propertiesFullFilename) { + if (!FileUtil.isExist(propertiesFullFilename)) { + FileUtil.createFile(propertiesFullFilename); + } + return getProperties(propertiesFullFilename); + } + + /** + * get properties + * + * @param propertiesFullFilename + * @return Properties + */ + public static Properties getProperties(String propertiesFullFilename) { + Properties properties = null; + if (propertiesFullFilename != null) { + InputStream inputStream = null; + try { + inputStream = new FileInputStream(propertiesFullFilename); + properties = new Properties(); + properties.load(inputStream); + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + } + return properties; + } + + /** + * get properties from properties file,will auto create + * + * @param file + * @return Properties + * @throws IOException + */ + public static Properties getProperties(File file) { + Properties properties = null; + if (file != null) { + properties = getProperties(file.getAbsolutePath()); + } + return properties; + } + + /** + * save properties + * + * @param properties + * @param outputFullFilename + */ + public static void saveProperties(Properties properties, String outputFullFilename) { + if (properties != null && outputFullFilename != null) { + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(outputFullFilename); + properties.store(outputStream, null); + } catch (Exception e) { + throw new FileUtilException(e); + } finally { + if (outputStream != null) { + try { + outputStream.flush(); + outputStream.close(); + } catch (Exception e) { + throw new FileUtilException(e); + } + } + } + } + } + + /** + * @param fromFile + * @param toFile + * @param fileCopyProcessor + */ + private static void copyFileToFile(final String fromFile, final String toFile, FileCopyProcessor fileCopyProcessor) { + if (fileCopyProcessor != null) { + createFile(toFile); + fileCopyProcessor.copyFileToFileProcess(fromFile, toFile, true); + } + } + + /** + * @param args + */ + public static void main(String[] args) { + String outputZipFullFilename = "/D:/a/b.zip"; + mergeZip(outputZipFullFilename, Arrays.asList("/D:/a.zip", "/D:/b.zip")); + } + + public enum FileCopyType { + PATH_TO_PATH, FILE_TO_PATH, FILE_TO_FILE + } + + public interface FileCopyProcessor { + + /** + * copyFileToFileProcess + * + * @param from,maybe directory + * @param to,maybe directory + * @param isFile,maybe directory or file + * @return boolean, if true keep going copy,only active in directory so + * far + */ + boolean copyFileToFileProcess(final String from, final String to, final boolean isFile); + + } + + public interface ZipProcessor { + + /** + * zip entry process + * + * @param zipEntryName + * @param inputStream + * @return InputStream + */ + InputStream zipEntryProcess(final String zipEntryName, InputStream inputStream); + } + + public interface CacheProcessor { + /** + * key process,can change key to save cache + * + * @param cacheKey + * @return String + */ + String keyProcess(final String key); + } + + public interface NoCacheFileProcessor { + /** + * process + * + * @param uncachedFileList + * @return boolean, true is save cache else false + */ + boolean process(List uncachedFileList); + } + + + public interface NoCacheFileFinder { + + /** + * find no cache file list + * + * @param cacheFileMapping + * @return List + */ + List findNoCacheFileList(Properties cacheFileMapping); + } + + public static class ZipEntryPath { + private String fullFilename = null; + private ZipEntry zipEntry = null; + private boolean replace = false; + + public ZipEntryPath(String fullFilename, ZipEntry zipEntry) { + this(fullFilename, zipEntry, false); + } + + public ZipEntryPath(String fullFilename, ZipEntry zipEntry, boolean replace) { + this.fullFilename = fullFilename; + this.zipEntry = zipEntry; + this.replace = replace; + } + } + + public static class FileUtilException extends RuntimeException { + private static final long serialVersionUID = 3884649425767533205L; + + public FileUtilException(Throwable cause) { + super(cause); + } + } + +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Generator.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Generator.java new file mode 100644 index 00000000..45bd622b --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/Generator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; + +public final class Generator { + + private static final char[] characters = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + private static final String FONT_FAMILY_TIMES_NEW_ROMAN = "Times New Roman"; + + /** + * md5 file + * + * @param fullFilename + * @return String + */ + public static String md5File(String fullFilename) { + String result = null; + if (fullFilename != null) { + try { + result = md5File(new FileInputStream(fullFilename)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return result; + } + + /** + * md5 file + * + * @param inputStream + * @return String + */ + public static String md5File(final InputStream inputStream) { + String result = null; + if (inputStream != null) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[Constant.Capacity.BYTES_PER_KB]; + int readCount = 0; + while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) { + md.update(buffer, 0, readCount); + } + result = StringUtil.byteToHexString(md.digest()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return result; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/JavaXmlUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/JavaXmlUtil.java new file mode 100644 index 00000000..1add7281 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/JavaXmlUtil.java @@ -0,0 +1,136 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import org.w3c.dom.Document; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +public final class JavaXmlUtil { + + /** + * get document builder + * + * @return DocumentBuilder + */ + private static DocumentBuilder getDocumentBuilder() { + DocumentBuilder documentBuilder = null; + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } + return documentBuilder; + } + + public static Document getEmptyDocument() { + Document document = null; + try { + DocumentBuilder documentBuilder = getDocumentBuilder(); + document = documentBuilder.newDocument(); + document.normalize(); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } + return document; + } + + /** + * parse + * + * @param filename + * @return Document + */ + public static Document parse(final String filename) { + Document document = null; + try { + DocumentBuilder documentBuilder = getDocumentBuilder(); + document = documentBuilder.parse(new File(filename)); + document.normalize(); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } + return document; + } + + /** + * parse + * + * @param inputStream + * @return Document + */ + public static Document parse(final InputStream inputStream) { + Document document = null; + try { + DocumentBuilder documentBuilder = getDocumentBuilder(); + document = documentBuilder.parse(inputStream); + document.normalize(); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } + return document; + } + + /** + * save document + * + * @param document + * @param outputFullFilename + */ + public static void saveDocument(final Document document, final String outputFullFilename) { + OutputStream outputStream = null; + try { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + DOMSource domSource = new DOMSource(document); + transformer.setOutputProperty(OutputKeys.ENCODING, Constant.Encoding.UTF8); + outputStream = new FileOutputStream(outputFullFilename); + StreamResult result = new StreamResult(outputStream); + transformer.transform(domSource, result); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception e) { + throw new JavaXmlUtilException(e); + } + } + } + } + + public static class JavaXmlUtilException extends RuntimeException { + private static final long serialVersionUID = 4669527982017700891L; + + public JavaXmlUtilException(Throwable cause) { + super(cause); + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ObjectUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ObjectUtil.java new file mode 100644 index 00000000..0802bde9 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ObjectUtil.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +/** + * reflect the object property and invoke the method + * + * @author Dandelion + * @since 2008-04-?? + */ +public final class ObjectUtil { + + private ObjectUtil() { + } + + /** + * when object is null return blank,when the object is not null it return object; + * + * @param object + * @return Object + */ + public static Object nullToBlank(Object object) { + if (object == null) { + return StringUtil.BLANK; + } + return object; + } + + /** + * equal + * + * @param a + * @param b + * @return boolean + */ + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * field name to method name + * + * @param methodPrefix + * @param fieldName + * @return methodName + */ + public static String fieldNameToMethodName(String methodPrefix, String fieldName) { + return fieldNameToMethodName(methodPrefix, fieldName, false); + } + + /** + * field name to method name + * + * @param methodPrefix + * @param fieldName + * @param ignoreFirstLetterCase + * @return methodName + */ + public static String fieldNameToMethodName(String methodPrefix, String fieldName, boolean ignoreFirstLetterCase) { + String methodName = null; + if (fieldName != null && fieldName.length() > 0) { + if (ignoreFirstLetterCase) { + methodName = methodPrefix + fieldName; + } else { + methodName = methodPrefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + } + } else { + methodName = methodPrefix; + } + return methodName; + } + + /** + * method name to field name + * + * @param methodPrefix + * @param methodName + * @return fieldName + */ + public static String methodNameToFieldName(String methodPrefix, String methodName) { + return methodNameToFieldName(methodPrefix, methodName, false); + } + + /** + * method name to field name + * + * @param methodPrefix + * @param methodName + * @param ignoreFirstLetterCase + * @return fieldName + */ + public static String methodNameToFieldName(String methodPrefix, String methodName, boolean ignoreFirstLetterCase) { + String fieldName = null; + if (methodName != null && methodName.length() > methodPrefix.length()) { + int front = methodPrefix.length(); + if (ignoreFirstLetterCase) { + fieldName = methodName.substring(front, front + 1) + methodName.substring(front + 1); + } else { + fieldName = methodName.substring(front, front + 1).toLowerCase() + methodName.substring(front + 1); + } + } + return fieldName; + } +} \ No newline at end of file diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/PatchUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/PatchUtil.java new file mode 100644 index 00000000..6cf80501 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/PatchUtil.java @@ -0,0 +1,195 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class PatchUtil { + + /** + * read r txt + * + * @param rTxtFullFilename + * @return Map> + */ + public static Map> readRTxt(String rTxtFullFilename) { + //read base resource entry + Map> rTypeResourceMap = new HashMap>(); + if (StringUtil.isNotBlank(rTxtFullFilename) && FileUtil.isExist(rTxtFullFilename)) { + BufferedReader bufferedReader = null; + try { + final Pattern textSymbolLine = Pattern.compile("(\\S+) (\\S+) (\\S+) (.+)"); + bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(rTxtFullFilename))); + String line = null; + while ((line = bufferedReader.readLine()) != null) { + Matcher matcher = textSymbolLine.matcher(line); + if (matcher.matches()) { + RDotTxtEntry.IdType idType = RDotTxtEntry.IdType.from(matcher.group(1)); + RDotTxtEntry.RType rType = RDotTxtEntry.RType.valueOf(matcher.group(2).toUpperCase()); + String name = matcher.group(3); + String idValue = matcher.group(4); + RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue); + Set hashSet = null; + if (rTypeResourceMap.containsKey(rType)) { + hashSet = rTypeResourceMap.get(rType); + } else { + hashSet = new HashSet(); + rTypeResourceMap.put(rType, hashSet); + } + hashSet.add(rDotTxtEntry); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + return rTypeResourceMap; + } + + /** + * generate public resource xml + * + * @param aaptResourceCollector + * @param outputIdsXmlFullFilename + * @param outputPublicXmlFullFilename + */ + public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector, String outputIdsXmlFullFilename, String outputPublicXmlFullFilename) { + if (aaptResourceCollector == null) { + return; + } + FileUtil.createFile(outputIdsXmlFullFilename); + FileUtil.createFile(outputPublicXmlFullFilename); + PrintWriter idsWriter = null; + PrintWriter publicWriter = null; + try { + FileUtil.createFile(outputIdsXmlFullFilename); + FileUtil.createFile(outputPublicXmlFullFilename); + idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), "UTF-8"); + publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), "UTF-8"); + idsWriter.println(""); + publicWriter.println(""); + idsWriter.println(""); + publicWriter.println(""); + Map> map = aaptResourceCollector.getRTypeResourceMap(); + Iterator>> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + RDotTxtEntry.RType rType = entry.getKey(); + if (!rType.equals(RDotTxtEntry.RType.STYLEABLE)) { + Set set = entry.getValue(); + for (RDotTxtEntry rDotTxtEntry : set) { +// if (rType.equals(RType.STYLE)) { + String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name); + if (StringUtil.isBlank(rawName)) { +// System.err.println("Blank?" + rDotTxtEntry.name); + rawName = rDotTxtEntry.name; + } + publicWriter.println(""); +// } else { +// publicWriter.println(""); +// } + } + Set ignoreIdSet = aaptResourceCollector.getIgnoreIdSet(); + for (RDotTxtEntry rDotTxtEntry : set) { + if (rType.equals(RDotTxtEntry.RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) { + idsWriter.println(""); + } else if (rType.equals(RDotTxtEntry.RType.STYLE)) { + + if (rDotTxtEntry.name.indexOf(Constant.Symbol.UNDERLINE) > 0) { +//idsWriter.println(""); + } + } + } + } + idsWriter.flush(); + publicWriter.flush(); + } + idsWriter.println(""); + publicWriter.println(""); + } catch (Exception e) { + throw new PatchUtilException(e); + } finally { + if (idsWriter != null) { + idsWriter.flush(); + idsWriter.close(); + } + if (publicWriter != null) { + publicWriter.flush(); + publicWriter.close(); + } + } + } + + public static class PublicResourceEntry { + private RDotTxtEntry.RType rType = null; + private String resourceName = null; + + public PublicResourceEntry(RDotTxtEntry.RType rType, String resourceName) { + this.rType = rType; + this.resourceName = resourceName; + } + + public boolean equals(Object obj) { + if (!(obj instanceof PublicResourceEntry)) { + return false; + } + PublicResourceEntry that = (PublicResourceEntry) obj; + return ObjectUtil.equal(this.rType, that.rType) && ObjectUtil.equal(this.resourceName, that.resourceName); + } + + public int hashCode() { + return Arrays.hashCode(new Object[]{this.rType, this.resourceName}); + } + } + + public static class PatchUtilException extends RuntimeException { + private static final long serialVersionUID = 5982003304074821184L; + + public PatchUtilException(String message) { + super(message); + } + + public PatchUtilException(Throwable cause) { + super(cause); + } + + public PatchUtilException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/RDotTxtEntry.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/RDotTxtEntry.java new file mode 100644 index 00000000..53cec1ee --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/RDotTxtEntry.java @@ -0,0 +1,146 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.ComparisonChain; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a row from a symbols file generated by {@code aapt}. + */ +public class RDotTxtEntry implements Comparable { + + private static final Pattern TEXT_SYMBOLS_LINE = Pattern.compile("(\\S+) (\\S+) (\\S+) (.+)"); + public static final Function TO_ENTRY = new Function() { + public RDotTxtEntry apply(String input) { + Optional entry = parse(input); + Preconditions.checkNotNull(entry.isPresent(), "Could not parse R.txt entry: '%s'", input); + return entry.get(); + } + }; + // A symbols file may look like: + // + // int id placeholder 0x7f020000 + // int string debug_http_proxy_dialog_title 0x7f030004 + // int string debug_http_proxy_hint 0x7f030005 + // int string debug_http_proxy_summary 0x7f030003 + // int string debug_http_proxy_title 0x7f030002 + // int string debug_ssl_cert_check_summary 0x7f030001 + // int string debug_ssl_cert_check_title 0x7f030000 + // + // Note that there are four columns of information: + // - the type of the resource id (always seems to be int or int[], in + // practice) + // - the type of the resource + // - the name of the resource + // - the value of the resource id + public final IdType idType; + public final RType type; + public final String name; + public String idValue; + public RDotTxtEntry(IdType idType, RType type, String name, String idValue) { + this.idType = Preconditions.checkNotNull(idType); + this.type = Preconditions.checkNotNull(type); + this.name = Preconditions.checkNotNull(name); + this.idValue = Preconditions.checkNotNull(idValue); + } + + public static Optional parse(String rDotTxtLine) { + Matcher matcher = TEXT_SYMBOLS_LINE.matcher(rDotTxtLine); + if (!matcher.matches()) { + return Optional.absent(); + } + + IdType idType = IdType.from(matcher.group(1)); + RType type = RType.valueOf(matcher.group(2).toUpperCase()); + String name = matcher.group(3); + String idValue = matcher.group(4); + + return Optional.of(new RDotTxtEntry(idType, type, name, idValue)); + } + + public RDotTxtEntry copyWithNewIdValue(String newIdValue) { + return new RDotTxtEntry(idType, type, name, newIdValue); + } + + /** + * A collection of Resources should be sorted such that Resources of the + * same type should be grouped together, and should be alphabetized within + * that group. + */ + public int compareTo(RDotTxtEntry that) { + return ComparisonChain.start().compare(this.type, that.type).compare(this.name, that.name).result(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RDotTxtEntry)) { + return false; + } + + RDotTxtEntry that = (RDotTxtEntry) obj; + return Objects.equal(this.type, that.type) && Objects.equal(this.name, that.name); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{type, name}); + } + + @Override + public String toString() { + return Objects.toStringHelper(RDotTxtEntry.class).add("idType", idType).add("type", type).add("name", name).add("idValue", idValue.trim()).toString(); + } + + // Taken from http://developer.android.com/reference/android/R.html + // TRANSITION for api level 19 + public enum RType { + ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, STRING, STYLE, STYLEABLE, TRANSITION, XML; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + } + + public enum IdType { + INT, INT_ARRAY; + + public static IdType from(String raw) { + if (raw.equals("int")) { + return INT; + } else if (raw.equals("int[]")) { + return INT_ARRAY; + } + throw new IllegalArgumentException(String.format("'%s' is not a valid ID type.", raw)); + } + + public String toString() { + if (this.equals(INT)) { + return "int"; + } + return "int[]"; + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceDirectory.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceDirectory.java new file mode 100644 index 00000000..e612953a --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceDirectory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class ResourceDirectory { + + public String directoryName = null; + public String resourceFullFilename = null; + public Set resourceEntrySet = new HashSet(); + + public ResourceDirectory(String directoryName, String resourceFullFilename) { + this.directoryName = directoryName; + this.resourceFullFilename = resourceFullFilename; + } + + public int hashCode() { + return Arrays.hashCode(new Object[]{this.directoryName, this.resourceFullFilename}); + } + + + public boolean equals(Object object) { + if (!(object instanceof ResourceDirectory)) { + return false; + } + ResourceDirectory that = (ResourceDirectory) object; + return ObjectUtil.equal(this.directoryName, that.directoryName) && ObjectUtil.equal(this.resourceFullFilename, that.resourceFullFilename); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceEntry.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceEntry.java new file mode 100644 index 00000000..8a4f5137 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/ResourceEntry.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.util.Arrays; + +public class ResourceEntry { + + public String name = null; + public String value = null; + + public ResourceEntry(String name, String value) { + this.name = name; + this.value = value; + } + + public int hashCode() { + return Arrays.hashCode(new Object[]{this.name}); + } + + + public boolean equals(Object object) { + if (!(object instanceof ResourceEntry)) { + return false; + } + ResourceEntry that = (ResourceEntry) object; + return ObjectUtil.equal(this.name, that.name); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/StringUtil.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/StringUtil.java new file mode 100644 index 00000000..7bc34b38 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/aapt/StringUtil.java @@ -0,0 +1,328 @@ +/* + * Copyright 2014-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package fastdex.build.lib.aapt; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class StringUtil { + + public static final String BLANK = ""; + public static final String SPACE = " "; + public static final String NULL = "null"; + public static final String CRLF_STRING = "\r\n"; + public static final byte CR = '\r'; + public static final byte LF = '\n'; + public static final byte[] CRLF = {CR, LF}; + private static final String METCH_PATTERN_REGEX = "[\\*]+"; + private static final String METCH_PATTERN = Constant.Symbol.WILDCARD; + private static final String METCH_PATTERN_REPLACEMENT = "[\\\\S|\\\\s]*"; + private static final String ZERO = "0"; + + private StringUtil() { + } + + /** + * when string is null return blank,where the string is not null it return string.trim + * + * @param string + * @return String + */ + public static String trim(final String string) { + String result = null; + if (string == null) { + result = BLANK; + } else { + result = string.trim(); + } + return result; + } + + /** + * when string is null return blank string + * + * @param string + * @return String + */ + public static String nullToBlank(final String string) { + return string == null ? BLANK : string; + } + + /** + * when string[] is null return blank array + * + * @param stringArray + * @return String[]{} length==0 + */ + public static String[] nullToBlank(final String[] stringArray) { + String[] result = stringArray; + if (stringArray == null) { + result = new String[]{}; + } + return result; + } + + /** + *

Checks if a String is whitespace, empty ("") or null.

+ *

+ *

+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * 
+ * + * @param string the String to check, may be null + * @return true if the String is null, empty or whitespace + */ + public static boolean isBlank(final String string) { + boolean result = false; + int strLen; + if (string == null || (strLen = string.length()) == 0) { + result = true; + } else { + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(string.charAt(i))) { + result = false; + break; + } + } + } + return result; + } + + /** + *

+ * Checks if a String is not empty (""), not null and not whitespace only. + *

+ *

+ *

+     * StringUtils.isNotBlank(null)      = false
+     * StringUtils.isNotBlank("")        = false
+     * StringUtils.isNotBlank(" ")       = false
+     * StringUtils.isNotBlank("bob")     = true
+     * StringUtils.isNotBlank("  bob  ") = true
+     * 
+ * + * @param string the String to check, may be null + * @return true if the String is not empty and not null and + * not whitespace + */ + public static boolean isNotBlank(final String string) { + return !isBlank(string); + } + + /** + * compare stringArray1 and stringArray2 return the different in str1 + * + * @param stringArray1 + * @param stringArray2 + * @return String[] + */ + public static String[] compareString(final String[] stringArray1, final String[] stringArray2) { + String[] differentString = null; + if (stringArray1 != null && stringArray2 != null) { + List list = new ArrayList(); + for (int i = 0; i < stringArray1.length; i++) { + boolean sign = false; + for (int j = 0; j < stringArray2.length; j++) { + if (stringArray1[i].equals(stringArray2[j])) { + sign = true; + break; + } + } + if (!sign) { + list.add(stringArray1[i]); + } + } + differentString = new String[list.size()]; + differentString = list.toArray(differentString); + } + return differentString; + } + + /** + *

Method:only for '*' match pattern,return true of false

+ * + * @param string + * @param patternString + * @return boolean + */ + public static boolean isMatchPattern(final String string, final String patternString) { + boolean result = false; + if (string != null && patternString != null) { + if (patternString.indexOf(METCH_PATTERN) >= 0) { + String matchPattern = Constant.Symbol.XOR + patternString.replaceAll(METCH_PATTERN_REGEX, METCH_PATTERN_REPLACEMENT) + Constant.Symbol.DOLLAR; + result = isMatchRegex(string, matchPattern); + } else { + if (string.equals(patternString)) { + result = true; + } + } + } + return result; + } + + /** + *

Method:only for regex

+ * + * @param string + * @param regex + * @return boolean + */ + public static boolean isMatchRegex(final String string, final String regex) { + boolean result = false; + if (string != null && regex != null) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(string); + result = matcher.find(); + } + return result; + } + + /** + *

Method:only for regex,parse regex group when regex include group

+ * + * @param string + * @param regex + * @return List + */ + public static List parseRegexGroup(final String string, final String regex) { + List groupList = null; + if (string != null && regex != null) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(string); + int groupCount = matcher.groupCount(); + int count = 1; + groupList = new ArrayList(); + if (matcher.find()) { + while (count <= groupCount) { + groupList.add(matcher.group(count)); + count++; + } + } + } + return groupList; + } + + /** + *

+ * Method: check the string match the regex or not and return the match + * field value + * like {xxxx} can find xxxx + *

+ * + * @param string + * @param regex + * @param firstRegex + * @param firstRegexReplace + * @param lastRegexStringLength like {xxxx},last regex string is "}" so last regex string length equals 1 + * @return List + */ + public static List parseStringGroup(final String string, final String regex, final String firstRegex, final String firstRegexReplace, final int lastRegexStringLength) { + List list = null; + if (string != null) { + list = new ArrayList(); + int lastRegexLength = lastRegexStringLength < 0 ? 0 : lastRegexStringLength; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(string); + String group = null; + int start = 0; + while (matcher.find(start)) { + start = matcher.end(); + group = matcher.group(); + group = group.replaceFirst(firstRegex, firstRegexReplace); + group = group.substring(0, group.length() - lastRegexLength); + list.add(group); + } + } + return list; + } + + /** + * byte to hex string + * + * @param byteArray + * @return String + */ + public static String byteToHexString(byte[] byteArray) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < byteArray.length; i++) { + int byteCode = byteArray[i] & 0xFF; + if (byteCode < 0x10) { + builder.append(0); + } + builder.append(Integer.toHexString(byteCode)); + } + return builder.toString(); + } + + /** + * hex string to byte + * + * @param source + * @return byte + */ + public static byte[] hexStringToByte(final String source) { + byte[] bytes = null; + if (source != null) { + bytes = new byte[source.length() / 2]; + int i = 0; + while (i < bytes.length) { + bytes[i] = (byte) (Integer.parseInt(source.substring(i * 2, (i + 1) * 2), 16)); + i++; + } + } + return bytes; + } + + /** + * fill zero + * + * @param length + * @return String + */ + public static String fillZero(int length) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < length; i++) { + stringBuilder.append(ZERO); + } + return stringBuilder.toString(); + } + + /** + *

Method: string mod operator,return 0~(mod-1)

+ * + * @param string + * @param mod + * @return int + */ + public static int stringMod(String string, int mod) { + int hashCode = 0; + if (string != null) { + hashCode = string.hashCode(); + if (hashCode < 0) { + hashCode = Math.abs(hashCode); + hashCode = hashCode < 0 ? 0 : hashCode; + } + } + return hashCode % (mod > 0 ? mod : 1); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/Communicator.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/Communicator.java new file mode 100644 index 00000000..1914c9bb --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/Communicator.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.build.lib.fd; + + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public abstract class Communicator { + + public abstract T communicate(DataInputStream input, DataOutputStream output) throws IOException; + + int getTimeout() { + return 2000; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ILogger.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ILogger.java new file mode 100644 index 00000000..beeca3a8 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ILogger.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.build.lib.fd; + +import java.util.Formatter; + +public interface ILogger { + + /** + * Prints an error message. + * + * @param t is an optional {@link Throwable} or {@link Exception}. If non-null, its + * message will be printed out. + * @param msgFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for errorFormat. + */ + void error( Throwable t, String msgFormat, Object... args); + + /** + * Prints a warning message. + * + * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null. + * @param args provides the arguments for warningFormat. + */ + void warning( String msgFormat, Object... args); + + /** + * Prints an information message. + * + * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null. + * @param args provides the arguments for msgFormat. + */ + void info( String msgFormat, Object... args); + + /** + * Prints a verbose message. + * + * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null. + * @param args provides the arguments for msgFormat. + */ + void verbose( String msgFormat, Object... args); + +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/NullLogger.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/NullLogger.java new file mode 100644 index 00000000..d7bc7304 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/NullLogger.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.build.lib.fd; + +/** + *

+ * Dummy implementation of an {@link ILogger}. + *

+ * Use {@link #getLogger()} to get a default instance of this {@link NullLogger}. + */ +public class NullLogger implements ILogger { + + private static final ILogger sThis = new NullLogger(); + + public static ILogger getLogger() { + return sThis; + } + + @Override + public void error( Throwable t, String errorFormat, Object... args) { + // ignore + } + + @Override + public void warning(String warningFormat, Object... args) { + // ignore + } + + @Override + public void info(String msgFormat, Object... args) { + // ignore + } + + @Override + public void verbose(String msgFormat, Object... args) { + // ignore + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ServiceCommunicator.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ServiceCommunicator.java new file mode 100644 index 00000000..bb20991b --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/fd/ServiceCommunicator.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.build.lib.fd; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.TimeoutException; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.util.Locale; + +import fastdex.common.fd.ProtocolConstants; + +/** + * Wrapper for talking to either the hotswap service or the run-as service. + */ +public class ServiceCommunicator { + + private static final String LOCAL_HOST = "127.0.0.1"; + + + private final String mPackageName; + + + private final ILogger mLogger; + + private final int mLocalPort; + + public ServiceCommunicator(String packageName) { + this(packageName,new NullLogger(),46628); + } + + public ServiceCommunicator(String packageName, int port) { + this(packageName,new NullLogger(),port); + } + + public ServiceCommunicator(String packageName, ILogger logger, int port) { + mPackageName = packageName; + mLogger = logger; + mLocalPort = port; + } + + public int getLocalPort() { + return mLocalPort; + } + + + public T talkToService(IDevice device, Communicator communicator) throws IOException { + try { + device.createForward(mLocalPort, mPackageName, IDevice.DeviceUnixSocketNamespace.ABSTRACT); + } catch (TimeoutException e) { + throw new IOException(e); + } catch (AdbCommandRejectedException e2) { + throw new IOException(e2); + } + try { + return talkToServiceWithinPortForward(communicator, mLocalPort); + } finally { + try { + device.removeForward(mLocalPort, mPackageName, + IDevice.DeviceUnixSocketNamespace.ABSTRACT); + } catch (IOException | TimeoutException | AdbCommandRejectedException e) { + // we don't worry that much about failures while removing port forwarding + mLogger.warning("Exception while removing port forward: " + e); + } + } + } + + private static T talkToServiceWithinPortForward(Communicator communicator, int localPort) throws IOException { + Socket socket = new Socket(LOCAL_HOST, localPort); + DataInputStream input = new DataInputStream(socket.getInputStream()); + DataOutputStream output = new DataOutputStream(socket.getOutputStream()); + output.writeLong(ProtocolConstants.PROTOCOL_IDENTIFIER); + output.writeInt(ProtocolConstants.PROTOCOL_VERSION); + + socket.setSoTimeout(2 * 1000); // Allow up to 2 seconds before timing out + int version = input.readInt(); + if (version != ProtocolConstants.PROTOCOL_VERSION) { + String msg = String.format(Locale.US, "Client and server protocol versions don't match (%1$d != %2$d)", version, ProtocolConstants.PROTOCOL_VERSION); + throw new IOException(msg); + } + socket.setSoTimeout(communicator.getTimeout()); + T value = communicator.communicate(input, output); + output.writeInt(ProtocolConstants.MESSAGE_EOF); + return value; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffInfo.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffInfo.java new file mode 100644 index 00000000..f1521129 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffInfo.java @@ -0,0 +1,56 @@ +package fastdex.build.lib.snapshoot.api; + +/** + * 目录对比,file.length或者file.lastModified不一样时判定文件发生变化 + * Created by tong on 17/3/29. + */ +public class DiffInfo { + public Status status; + public String uniqueKey; + public T now;//如果是删除此值为null + public T old; + + public DiffInfo() { + } + + public DiffInfo(Status status, String uniqueKey, T now, T old) { + this.status = status; + this.uniqueKey = uniqueKey; + + this.now = now; + this.old = old; + + if (this.uniqueKey == null || this.uniqueKey.length() == 0) { + throw new IllegalStateException("UniqueKey can not be null or epmty!!"); + } + } + + @Override + public String toString() { + return "DiffInfo{" + + "status=" + status + + ", uniqueKey='" + uniqueKey + '\'' + + ", now=" + now + + ", old=" + old + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DiffInfo that = (DiffInfo) o; + + if (status != that.status) return false; + return uniqueKey != null ? uniqueKey.equals(that.uniqueKey) : that.uniqueKey == null; + + } + + @Override + public int hashCode() { + int result = status != null ? status.hashCode() : 0; + result = 31 * result + (uniqueKey != null ? uniqueKey.hashCode() : 0); + return result; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffResultSet.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffResultSet.java new file mode 100644 index 00000000..d398083f --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/DiffResultSet.java @@ -0,0 +1,151 @@ +package fastdex.build.lib.snapshoot.api; + +import fastdex.common.utils.SerializeUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by tong on 17/3/30. + */ +public class DiffResultSet implements STSerializable { + public Set changedDiffInfos = new HashSet(); + public Set nochangedDiffInfos = new HashSet(); + + public DiffResultSet() { + } + + public DiffResultSet(DiffResultSet resultSet) { + changedDiffInfos.addAll(resultSet.changedDiffInfos); + nochangedDiffInfos.addAll(resultSet.nochangedDiffInfos); + } + + /** + * 添加对比信息 + * @param diffInfo + * @return + */ + public boolean add(T diffInfo) { + if (diffInfo == null) { + return false; + } + if (diffInfo.status == Status.NOCHANGED) { + if (nochangedDiffInfos == null) { + nochangedDiffInfos = new HashSet(); + } + return nochangedDiffInfos.add(diffInfo); + } + if (changedDiffInfos == null) { + changedDiffInfos = new HashSet(); + } + return changedDiffInfos.add(diffInfo); + } + + /** + * 合并结果集 + * @param resultSet + */ + public void merge(DiffResultSet resultSet) { + if (changedDiffInfos == null) { + changedDiffInfos = new HashSet(); + } + changedDiffInfos.addAll(resultSet.changedDiffInfos); + } + + /** + * 获取所有发生变化的结果集 + * @return + */ + public Set getAllChangedDiffInfos() { + HashSet set = new HashSet<>(); + set.addAll(changedDiffInfos); + return set; + } + + /** + * 获取所有发生变化的结果集 + * @return + */ + public Set getAllNochangedDiffInfos() { + HashSet set = new HashSet<>(); + set.addAll(nochangedDiffInfos); + return set; + } + + public Set getDiffInfos(Status ...statuses) { + Set result = new HashSet(); + if (statuses == null || statuses.length == 0) { + result.addAll(changedDiffInfos); + result.addAll(nochangedDiffInfos); + return result; + } + + for (T diffInfo : changedDiffInfos) { + bb : for (Status status : statuses) { + if (diffInfo.status == status) { + result.add(diffInfo); + break bb; + } + } + } + + boolean containNochangedStatus = false; + for (Status status : statuses) { + if (status == Status.NOCHANGED) { + containNochangedStatus = true; + break; + } + } + if (containNochangedStatus) { + result.addAll(nochangedDiffInfos); + } + return result; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DiffResultSet resultSet = (DiffResultSet) o; + + return changedDiffInfos != null ? changedDiffInfos.equals(resultSet.changedDiffInfos) : resultSet.changedDiffInfos == null; + + } + + @Override + public int hashCode() { + return changedDiffInfos != null ? changedDiffInfos.hashCode() : 0; + } + + @Override + public String toString() { + return "DiffResultSet{" + + "changedJavaFileDiffInfos=" + changedDiffInfos + + '}'; + } + + @Override + public void serializeTo(OutputStream outputStream) throws IOException { + SerializeUtils.serializeTo(outputStream,this); + } + + public static DiffResultSet load(InputStream inputStream, Class type) throws Exception { + DiffResultSet resultSet = (DiffResultSet) SerializeUtils.load(inputStream,type); + if (resultSet != null) { + Constructor constructor = type.getConstructor(resultSet.getClass()); + resultSet = (DiffResultSet) constructor.newInstance(resultSet); + } + return resultSet; + } + + public static DiffResultSet load(File file, Class type) throws Exception { + return load(new FileInputStream(file),type); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Node.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Node.java new file mode 100644 index 00000000..beac4a04 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Node.java @@ -0,0 +1,38 @@ +package fastdex.build.lib.snapshoot.api; + +/** + * Created by tong on 17/3/29. + */ +public abstract class Node { + /** + * 获取索引值 + * @return + */ + public abstract String getUniqueKey(); + /** + * 如果没有发生变化返回true,反之false + * @param anNode + * @return + */ + public boolean diffEquals(Node anNode) { + return equals(anNode); + } + + @Override + public final boolean equals(Object o) { + if (super.equals(o)) return true; + if (o == null || getClass() != o.getClass()) return false; + + Node node = (Node) o; + + String uniqueKey = getUniqueKey(); + String anUniqueKey = node.getUniqueKey(); + return uniqueKey != null ? uniqueKey.equals(anUniqueKey) : anUniqueKey == null; + } + + @Override + public final int hashCode() { + String uniqueKey = getUniqueKey(); + return uniqueKey != null ? uniqueKey.hashCode() : 0; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/STSerializable.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/STSerializable.java new file mode 100644 index 00000000..1d2ec870 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/STSerializable.java @@ -0,0 +1,11 @@ +package fastdex.build.lib.snapshoot.api; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by tong on 17/3/30. + */ +public interface STSerializable { + void serializeTo(OutputStream outputStream) throws IOException; +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Snapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Snapshoot.java new file mode 100644 index 00000000..063ba6dc --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Snapshoot.java @@ -0,0 +1,230 @@ +package fastdex.build.lib.snapshoot.api; + +import fastdex.common.utils.SerializeUtils; +import com.google.gson.annotations.Expose; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.util.*; + +/** + * Created by tong on 17/3/29. + */ +public class Snapshoot implements STSerializable { + public Collection nodes; + + @Expose + private DiffResultSet lastDiffResult; + + public Snapshoot() { + createEmptyNodes(); + } + + public Snapshoot(Snapshoot snapshoot) { + createEmptyNodes(); + nodes.addAll(snapshoot.getAllNodes()); + } + + protected void createEmptyNodes() { + nodes = new HashSet(); + } + + /** + * 创建空的对比结果集 + * @return + */ + protected DiffResultSet createEmptyResultSet() { + return new DiffResultSet(); + } + + protected DiffInfo createEmptyDiffInfo() { + return new DiffInfo(); + } + + /** + * 添加内容 + * @param code + */ + protected void addNode(NODE code) { + nodes.add(code); + } + + /** + * 获取所有的内容 + * @return + */ + protected Collection getAllNodes() { + return nodes; + } + + public DiffResultSet getLastDiffResult() { + return lastDiffResult; + } + + /** + * 通过索引获取内容 + * @param uniqueKey + * @return + */ + protected NODE getNodeByUniqueKey(String uniqueKey) { + NODE node = null; + for (NODE n : nodes) { + if (uniqueKey.equals(n.getUniqueKey())) { + node = n; + break; + } + } + return node; + } + + /** + * 创建一项内容的对比结果 + * @param status + * @param now + * @param old + * @return + */ + protected DIFF_INFO createDiffInfo(Status status, NODE now, NODE old) { + DIFF_INFO diffInfo = (DIFF_INFO) createEmptyDiffInfo(); + diffInfo.status = status; + diffInfo.now = now; + diffInfo.old = old; + + switch (status) { + case NOCHANGED: + case ADDED: + case MODIFIED: + diffInfo.uniqueKey = now.getUniqueKey(); + break; + case DELETEED: + diffInfo.uniqueKey = old.getUniqueKey(); + break; + } + + return diffInfo; + } + + /** + * 把一项内容的对比结果添加到结果集中 + * @param diffInfos + * @param diffInfo + */ + protected void addDiffInfo(DiffResultSet diffInfos, DIFF_INFO diffInfo) { + diffInfos.add(diffInfo); + } + + /** + * 对比快照 + * @param otherSnapshoot + * @return + */ + public DiffResultSet diff(Snapshoot otherSnapshoot) { + //获取删除项 + Set deletedNodes = new HashSet<>(); + deletedNodes.addAll(otherSnapshoot.getAllNodes()); + deletedNodes.removeAll(new ArrayList(getAllNodes())); + + //新增项 + Set increasedNodes = new HashSet<>(); + increasedNodes.addAll(getAllNodes()); + //如果不用ArrayList套一层有时候会发生移除不掉的情况 why? + increasedNodes.removeAll(new ArrayList(otherSnapshoot.getAllNodes())); + + //需要检测是否变化的列表 + Set needDiffNodes = new HashSet<>(); + needDiffNodes.addAll(getAllNodes()); + needDiffNodes.addAll(otherSnapshoot.getAllNodes()); + needDiffNodes.removeAll(new ArrayList(deletedNodes)); + needDiffNodes.removeAll(new ArrayList(increasedNodes)); + + DiffResultSet diffInfos = createEmptyResultSet(); + scanDeletedAndIncreased(diffInfos,otherSnapshoot,deletedNodes,new HashSet(increasedNodes)); + scanNeedDiffNodes(diffInfos,otherSnapshoot,needDiffNodes); + + this.lastDiffResult = diffInfos; + return diffInfos; + } + + /** + * 扫描变化项和删除项 + * @param diffInfos + * @param otherSnapshoot + * @param deletedNodes + * @param increasedNodes + */ + protected void scanDeletedAndIncreased(DiffResultSet diffInfos, Snapshoot otherSnapshoot, Set deletedNodes, Set increasedNodes) { + if (deletedNodes != null) { + for (NODE node : deletedNodes) { + addDiffInfo(diffInfos,createDiffInfo(Status.DELETEED,null,node)); + } + } + if (increasedNodes != null) { + for (NODE node : increasedNodes) { + addDiffInfo(diffInfos,createDiffInfo(Status.ADDED,node,null)); + } + } + } + + /** + * 对比两个node + * @param diffInfos + * @param otherSnapshoot + * @param now + * @param old + */ + protected void diffNode(DiffResultSet diffInfos, Snapshoot otherSnapshoot, NODE now, NODE old) { + if (now.diffEquals(old)) { + addDiffInfo(diffInfos,createDiffInfo(Status.NOCHANGED,now,old)); + } + else { + addDiffInfo(diffInfos,createDiffInfo(Status.MODIFIED,now,old)); + } + } + + /** + * 扫描uniqueKey相同的node + * @param diffInfos + * @param otherSnapshoot + * @param needDiffNodes + */ + protected void scanNeedDiffNodes(DiffResultSet diffInfos, Snapshoot otherSnapshoot, Set needDiffNodes) { + if (needDiffNodes == null || needDiffNodes.isEmpty()) { + return; + } + for (NODE node : needDiffNodes) { + NODE now = node; + String uniqueKey = node.getUniqueKey(); + if (uniqueKey == null || uniqueKey.length() == 0) { + throw new RuntimeException("UniqueKey can not be null or empty!!"); + } + NODE old = otherSnapshoot.getNodeByUniqueKey(uniqueKey); + diffNode(diffInfos,otherSnapshoot,now,old); + } + } + + @Override + public void serializeTo(OutputStream outputStream) throws IOException { + SerializeUtils.serializeTo(outputStream,this); + } + + public static Snapshoot load(InputStream inputStream, Class type) throws Exception { + Snapshoot snapshoot = (Snapshoot) SerializeUtils.load(inputStream,type); + if (snapshoot != null) { + Constructor constructor = type.getConstructor(snapshoot.getClass()); + snapshoot = (Snapshoot) constructor.newInstance(snapshoot); + } + return snapshoot; + } + + public static Snapshoot load(File path, Class type) throws Exception { + Snapshoot snapshoot = (Snapshoot) SerializeUtils.load(new FileInputStream(path),type); + if (snapshoot != null) { + Constructor constructor = type.getConstructor(snapshoot.getClass()); + snapshoot = (Snapshoot) constructor.newInstance(snapshoot); + } + return snapshoot; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Status.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Status.java new file mode 100644 index 00000000..4860def5 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/api/Status.java @@ -0,0 +1,8 @@ +package fastdex.build.lib.snapshoot.api; + +/** + * Created by tong on 17/3/29. + */ +public enum Status { + NOCHANGED, ADDED, DELETEED, MODIFIED +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/BaseDirectorySnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/BaseDirectorySnapshoot.java new file mode 100644 index 00000000..be686ce2 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/BaseDirectorySnapshoot.java @@ -0,0 +1,144 @@ +package fastdex.build.lib.snapshoot.file; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.api.Snapshoot; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; + +/** + * 目录快照 + * Created by tong on 17/3/29. + */ +public class BaseDirectorySnapshoot extends Snapshoot { + public String path; + + public BaseDirectorySnapshoot() { + } + + public BaseDirectorySnapshoot(BaseDirectorySnapshoot snapshoot) { + super(snapshoot); + this.path = snapshoot.path; + } + + public BaseDirectorySnapshoot(File directory) throws IOException { + this(directory,(ScanFilter)null); + } + + public BaseDirectorySnapshoot(File directory, ScanFilter scanFilter) throws IOException { + if (directory == null) { + throw new IllegalArgumentException("Directory can not be null!!"); + } +// if (!directory.exists() || !directory.isDirectory()) { +// throw new IllegalArgumentException("Invalid directory: " + directory); +// } + this.path = directory.getAbsolutePath(); + + if (directory.exists() && directory.isDirectory()) { + walkFileTree(directory,scanFilter); + } + } + + public BaseDirectorySnapshoot(File directory, String ...childPath) throws IOException { + if (directory == null) { + throw new IllegalArgumentException("Directory can not be null!!"); + } +// if (!directory.exists() || !directory.isDirectory()) { +// throw new IllegalArgumentException("Invalid directory: " + directory); +// } + this.path = directory.getAbsolutePath(); + + if (childPath != null) { + for (String path : childPath) { + if (path != null) { + visitFile(new File(path).toPath(),null,null); + } + } + } + } + + public BaseDirectorySnapshoot(File directory, Collection childPath) throws IOException { + if (directory == null) { + throw new IllegalArgumentException("Directory can not be null!!"); + } +// if (!directory.exists() || !directory.isDirectory()) { +// throw new IllegalArgumentException("Invalid directory: " + directory); +// } + + this.path = directory.getAbsolutePath(); + + if (childPath != null) { + for (File f : childPath) { + if (f != null) { + visitFile(f.toPath(),null,null); + } + } + } + } + + @Override + protected DiffInfo createEmptyDiffInfo() { + return new FileDiffInfo(); + } + + protected void walkFileTree(File directory, final ScanFilter scanFilter) throws IOException { + Files.walkFileTree(directory.toPath(),new SimpleFileVisitor(){ + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + return BaseDirectorySnapshoot.this.visitFile(file,attrs,scanFilter); + } + }); + } + + protected FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs,ScanFilter scanFilter) throws IOException { + if (scanFilter != null) { + if (!scanFilter.preVisitFile(filePath.toFile())) { + return FileVisitResult.CONTINUE; + } + } + addNode((NODE) FileNode.create(new File(path),filePath.toFile())); + return FileVisitResult.CONTINUE; + } + + public File getAbsoluteFile(FileNode fileItemInfo) { + return new File(path,fileItemInfo.getUniqueKey()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BaseDirectorySnapshoot that = (BaseDirectorySnapshoot) o; + + return path != null ? path.equals(that.path) : that.path == null; + } + + + + @Override + public int hashCode() { + return path != null ? path.hashCode() : 0; + } + + public static DiffResultSet diff(File now, File old) throws IOException { + return BaseDirectorySnapshoot.diff(now,old,null); + } + + public static DiffResultSet diff(File now, File old, ScanFilter scanFilter) throws IOException { + return new BaseDirectorySnapshoot(now,scanFilter).diff(new BaseDirectorySnapshoot(old,scanFilter)); + } + + @Override + public String toString() { + return "BaseDirectorySnapshoot{" + + "path='" + path + '\'' + + '}'; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/DirectorySnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/DirectorySnapshoot.java new file mode 100644 index 00000000..3cfe4375 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/DirectorySnapshoot.java @@ -0,0 +1,60 @@ +package fastdex.build.lib.snapshoot.file; + +/** + + 当前 + com/dx168/fastdex/sample/MainActivity.java + 老的 + com/dx168/fastdex/sample/MainActivity.java + com/dx168/fastdex/sample/MainActivity2.java + 删除的是 + com/dx168/fastdex/sample/MainActivity2.java + + 假如 + com/dx168/fastdex/sample/MainActivity.java + com/dx168/fastdex/sample/MainActivity2.java + 老的 + com/dx168/fastdex/sample/MainActivity.java + 新增的是 + com/dx168/fastdex/sample/MainActivity2.java + + 当前的 + com/dx168/fastdex/sample/MainActivity.java + com/dx168/fastdex/sample/MainActivity2.java + com/dx168/fastdex/sample/MainActivity3.java + 老的 + com/dx168/fastdex/sample/MainActivity.java + com/dx168/fastdex/sample/MainActivity2.java + com/dx168/fastdex/sample/MainActivity4.java + 新增的是 + com/dx168/fastdex/sample/MainActivity3.java + 删除的是 + com/dx168/fastdex/sample/MainActivity4.java + + 除了删除的和新增的就是所有需要进行扫描的SourceSetInfo + com/dx168/fastdex/sample/MainActivity.java + com/dx168/fastdex/sample/MainActivity2.java + */ + +import java.io.File; +import java.io.IOException; + +/** + * Created by tong on 17/3/29. + */ +public final class DirectorySnapshoot extends BaseDirectorySnapshoot { + public DirectorySnapshoot() { + } + + public DirectorySnapshoot(BaseDirectorySnapshoot snapshoot) { + super(snapshoot); + } + + public DirectorySnapshoot(File directory) throws IOException { + super(directory); + } + + public DirectorySnapshoot(File directory, ScanFilter scanFilter) throws IOException { + super(directory, scanFilter); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileDiffInfo.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileDiffInfo.java new file mode 100644 index 00000000..e8928880 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileDiffInfo.java @@ -0,0 +1,17 @@ +package fastdex.build.lib.snapshoot.file; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.Status; + +/** + * 目录对比,file.length或者file.lastModified不一样时判定文件发生变化 + * Created by tong on 17/3/29. + */ +public class FileDiffInfo extends DiffInfo { + public FileDiffInfo() { + } + + public FileDiffInfo(Status status, FileNode now, FileNode old) { + super(status, (now != null ? now.getUniqueKey() : old.getUniqueKey()), now, old); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileNode.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileNode.java new file mode 100644 index 00000000..39b15d07 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileNode.java @@ -0,0 +1,51 @@ +package fastdex.build.lib.snapshoot.file; + +import fastdex.build.lib.snapshoot.api.Node; + +import java.io.File; + +/** + * Created by tong on 17/3/29. + */ +public class FileNode extends Node { + //public String absolutePath; + public String relativePath; + public long lastModified; + public long fileLength; + + @Override + public String getUniqueKey() { + return relativePath; + } + + @Override + public boolean diffEquals(Node anNode) { + if (this == anNode) return true; + if (anNode == null) return false; + + FileNode fileNode = (FileNode) anNode; + if (lastModified != fileNode.lastModified) return false; + if (fileLength != fileNode.fileLength) return false; + return equals(fileNode); + } + + @Override + public String toString() { + return "FileNode{" + + "relativePath='" + relativePath + '\'' + + ", lastModified=" + lastModified + + ", fileLength=" + fileLength + + '}'; + } + + public static FileNode create(File rootDir, File file) { + //相对路径作为key + FileNode fileInfo = new FileNode(); + //fileInfo.absolutePath = file.getAbsolutePath(); + fileInfo.relativePath = rootDir.toPath().relativize(file.toPath()).toString(); + + fileInfo.lastModified = file.lastModified(); + fileInfo.fileLength = file.length(); + return fileInfo; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileSuffixFilter.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileSuffixFilter.java new file mode 100644 index 00000000..63f8e043 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/FileSuffixFilter.java @@ -0,0 +1,47 @@ +package fastdex.build.lib.snapshoot.file; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by tong on 17/3/29. + */ +public class FileSuffixFilter implements ScanFilter { + private final Set suffixList = new HashSet<>(); + + public FileSuffixFilter(String suffix) { + addSuffix(suffix); + } + + public FileSuffixFilter(Set suffixList) { + for (String suffix : suffixList) { + addSuffix(suffix); + } + } + + public void addSuffix(String suffix) { + if (suffix == null || suffix.length() == 0) { + throw new IllegalArgumentException("suffix can not be epmty!!"); + } + this.suffixList.add(suffix); + + if (this.suffixList.isEmpty()) { + throw new IllegalArgumentException("suffix list can not be epmty!!"); + } + } + + public Set getSuffixList() { + return suffixList; + } + + @Override + public boolean preVisitFile(File file) { + for (String suffix : suffixList) { + if (file.getName().endsWith(suffix)) { + return true; + } + } + return false; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/Options.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/Options.java new file mode 100644 index 00000000..bb6c628e --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/Options.java @@ -0,0 +1,47 @@ +package fastdex.build.lib.snapshoot.file; + +import fastdex.build.lib.snapshoot.api.Status; + +import java.util.HashSet; +import java.util.Set; + +/** + * Created by tong on 17/3/30. + */ +public class Options { + private final Set suffixList = new HashSet<>(); + private Status[] focusStatus = null; + + public static class Builder { + private final Options options = new Options(); + + public Builder addSuffix(String suffix) { + options.suffixList.add(suffix); + return this; + } + + public Builder focusStatus(Status ...focusStatus) { + if (focusStatus != null) { + Set set = new HashSet(); + for (Status status : focusStatus) { + set.add(status); + } + if (set.size() < focusStatus.length) { + throw new IllegalStateException("Content can not be repeated !"); + } + } + + if (focusStatus.length == 0) { + options.focusStatus = null; + } + else { + options.focusStatus = focusStatus; + } + return this; + } + + public Options build() { + return options; + } + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/ScanFilter.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/ScanFilter.java new file mode 100644 index 00000000..eeb8d0bb --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/file/ScanFilter.java @@ -0,0 +1,15 @@ +package fastdex.build.lib.snapshoot.file; + +import java.io.File; + +/** + * Created by tong on 17/3/29. + */ +public interface ScanFilter { + /** + * 如果返回true处理这个文件,反之忽略 + * @param file + * @return + */ + boolean preVisitFile(File file); +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectoryDiffResultSet.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectoryDiffResultSet.java new file mode 100644 index 00000000..1e1e82e3 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectoryDiffResultSet.java @@ -0,0 +1,17 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import fastdex.build.lib.snapshoot.api.DiffResultSet; + +/** + * Created by tong on 17/3/29. + */ +public class JavaDirectoryDiffResultSet extends DiffResultSet { + public String projectPath; + + public JavaDirectoryDiffResultSet() { + } + + public JavaDirectoryDiffResultSet(JavaDirectoryDiffResultSet resultSet) { + super(resultSet); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectorySnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectorySnapshoot.java new file mode 100644 index 00000000..ae539940 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaDirectorySnapshoot.java @@ -0,0 +1,55 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import fastdex.build.lib.snapshoot.file.BaseDirectorySnapshoot; +import fastdex.build.lib.snapshoot.file.FileNode; +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.file.FileSuffixFilter; +import fastdex.build.lib.snapshoot.file.ScanFilter; +import com.google.gson.annotations.Expose; +import java.io.File; +import java.io.IOException; +import java.util.Collection; + +/** + * Created by tong on 17/3/30. + */ +public class JavaDirectorySnapshoot extends BaseDirectorySnapshoot { + private static final FileSuffixFilter JAVA_SUFFIX_FILTER = new FileSuffixFilter(".java"); + @Expose + public String projectPath; + + public JavaDirectorySnapshoot() { + } + + public JavaDirectorySnapshoot(JavaDirectorySnapshoot snapshoot) { + super(snapshoot); + } + + public JavaDirectorySnapshoot(File directory) throws IOException { + super(directory, JAVA_SUFFIX_FILTER); + } + + public JavaDirectorySnapshoot(File directory, ScanFilter scanFilter) throws IOException { + super(directory, scanFilter); + } + + public JavaDirectorySnapshoot(File directory, String ...childPath) throws IOException { + super(directory, childPath); + } + + public JavaDirectorySnapshoot(File directory, Collection childPath) throws IOException { + super(directory, childPath); + } + + @Override + protected JavaDirectoryDiffResultSet createEmptyResultSet() { + JavaDirectoryDiffResultSet javaDirectoryDiffResultSet = new JavaDirectoryDiffResultSet(); + javaDirectoryDiffResultSet.projectPath = projectPath; + return javaDirectoryDiffResultSet; + } + + @Override + protected DiffInfo createEmptyDiffInfo() { + return new JavaFileDiffInfo(); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaFileDiffInfo.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaFileDiffInfo.java new file mode 100644 index 00000000..f6c6d8c3 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/JavaFileDiffInfo.java @@ -0,0 +1,66 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import fastdex.build.lib.snapshoot.api.Status; +import fastdex.build.lib.snapshoot.file.FileDiffInfo; +import fastdex.build.lib.snapshoot.file.FileNode; + +/** + * 目录对比,file.length或者file.lastModified不一样时判定文件发生变化 + * Created by tong on 17/3/29. + */ +public class JavaFileDiffInfo extends FileDiffInfo { + public JavaFileDiffInfo() { + } + + public JavaFileDiffInfo(Status status, FileNode now, FileNode old) { + super(status, now, old); + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) return false; + if (o == null || getClass() != o.getClass()) return false; + + JavaFileDiffInfo that = (JavaFileDiffInfo) o; + + if (status != that.status) return false; + if (uniqueKey != null ? !uniqueKey.equals(that.uniqueKey) : that.uniqueKey != null) return false; + + if (now != null && !now.diffEquals(that.now)) { + return false; + } + + if (old != null && !old.diffEquals(that.old)) { + return false; + } + return true; + + } + + @Override + public int hashCode() { + int result = super.hashCode(); + int nowLastModified = 0; + + if (now != null) { + nowLastModified = (int) (((FileNode)now).lastModified ^ (((FileNode)now).lastModified >>> 32)); + } + + int oldLastModified = 0; + if (old != null) { + oldLastModified = (int) (((FileNode)old).lastModified ^ (((FileNode)old).lastModified >>> 32)); + } + + int nowFileLength = 0; + int oldFileLength = 0; + if (now != null) { + nowFileLength = (int) (((FileNode)now).fileLength ^ (((FileNode)now).fileLength >>> 32)); + } + if (old != null) { + oldFileLength = (int) (((FileNode)old).fileLength ^ (((FileNode)old).fileLength >>> 32)); + } + result = 31 * result + (now != null ? (now.hashCode() + nowLastModified + nowFileLength) : 0); + result = 31 * result + (old != null ? (old.hashCode() + oldLastModified + oldFileLength) : 0); + return result; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/PathInfo.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/PathInfo.java new file mode 100644 index 00000000..91872913 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/PathInfo.java @@ -0,0 +1,44 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import java.io.File; + +/** + * Created by tong on 17/4/6. + */ +public class PathInfo { + public File absoluteFile; + public String relativePath; + + public PathInfo(File absoluteFile, String relativePath) { + this.absoluteFile = absoluteFile; + this.relativePath = relativePath; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PathInfo pathInfo = (PathInfo) o; + + if (absoluteFile != null ? !absoluteFile.equals(pathInfo.absoluteFile) : pathInfo.absoluteFile != null) + return false; + return relativePath != null ? relativePath.equals(pathInfo.relativePath) : pathInfo.relativePath == null; + + } + + @Override + public int hashCode() { + int result = absoluteFile != null ? absoluteFile.hashCode() : 0; + result = 31 * result + (relativePath != null ? relativePath.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PathInfo{" + + "absoluteFile=" + absoluteFile + + ", relativePath='" + relativePath + '\'' + + '}'; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetDiffResultSet.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetDiffResultSet.java new file mode 100644 index 00000000..e973a8a7 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetDiffResultSet.java @@ -0,0 +1,116 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.api.Status; +import fastdex.build.lib.snapshoot.string.StringDiffInfo; +import com.google.gson.annotations.Expose; + +import org.apache.tools.ant.taskdefs.condition.Os; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Created by tong on 17/3/31. + */ +public class SourceSetDiffResultSet extends DiffResultSet { + public Set changedJavaFileDiffInfos = new HashSet(); + + @Expose + public Set addOrModifiedClasses = new HashSet<>(); + + @Expose + public Set addOrModifiedPathInfos = new HashSet<>(); + + @Expose + public Map> addOrModifiedClassesMap = new HashMap<>(); + + public SourceSetDiffResultSet() { + + } + + public SourceSetDiffResultSet(SourceSetDiffResultSet resultSet) { + super(resultSet); + //from gson + this.changedJavaFileDiffInfos.addAll(resultSet.changedJavaFileDiffInfos); + this.addOrModifiedClasses.addAll(resultSet.addOrModifiedClasses); + this.addOrModifiedPathInfos.addAll(resultSet.addOrModifiedPathInfos); + } + + public boolean isJavaFileChanged() { + return !addOrModifiedClasses.isEmpty(); + } + +// public void addJavaFileDiffInfo(JavaFileDiffInfo diffInfo) { +// if (diffInfo.status != Status.NOCHANGED) { +// this.changedJavaFileDiffInfos.add(diffInfo); +// } +// } + + public void mergeJavaDirectoryResultSet(String path,JavaDirectoryDiffResultSet javaDirectoryResultSet) { + List addOrModifiedClassRelativePathList = addOrModifiedClassesMap.get(javaDirectoryResultSet.projectPath); + if (addOrModifiedClassRelativePathList == null) { + addOrModifiedClassRelativePathList = new ArrayList<>(); + addOrModifiedClassesMap.put(javaDirectoryResultSet.projectPath,addOrModifiedClassRelativePathList); + } + + for (JavaFileDiffInfo javaFileDiffInfo : javaDirectoryResultSet.changedDiffInfos) { + switch (javaFileDiffInfo.status) { + case ADDED: + case MODIFIED: + addOrModifiedPathInfos.add(new PathInfo(new File(path,javaFileDiffInfo.uniqueKey),javaFileDiffInfo.uniqueKey)); + String classRelativePath = javaFileDiffInfo.uniqueKey.substring(0, javaFileDiffInfo.uniqueKey.length() - ".java".length()); + +// String entryName = classRelativePath; +// if (entryName.contains("\\")) { +// entryName = entryName.replace("\\", "/"); +// } +// entryName = entryName + ".class"; +// addOrModifiedClassRelativePathList.add(entryName); + addOrModifiedClassRelativePathList.add(classRelativePath + ".class"); + addOrModifiedClassRelativePathList.add(classRelativePath + "$*.class"); + + classRelativePath = classRelativePath.replaceAll(Os.isFamily(Os.FAMILY_WINDOWS) ? "\\\\" : File.separator,"\\."); + addOrModifiedClasses.add(classRelativePath); + +// addOrModifiedClasses.add(classRelativePath + ".class"); +// addOrModifiedClasses.add(classRelativePath + "\\$\\S{0,}.class"); + break; + } + } + this.changedJavaFileDiffInfos.addAll(javaDirectoryResultSet.changedDiffInfos); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + SourceSetDiffResultSet resultSet = (SourceSetDiffResultSet) o; + + return changedJavaFileDiffInfos != null ? changedJavaFileDiffInfos.equals(resultSet.changedJavaFileDiffInfos) : resultSet.changedJavaFileDiffInfos == null; + + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (changedJavaFileDiffInfos != null ? changedJavaFileDiffInfos.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SourceSetDiffResultSet{" + + "changedJavaFileDiffInfos=" + changedJavaFileDiffInfos + + ", addOrModifiedClasses=" + addOrModifiedClasses + + ", addOrModifiedPathInfos=" + addOrModifiedPathInfos + + '}'; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetSnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetSnapshoot.java new file mode 100644 index 00000000..c43296c6 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/sourceset/SourceSetSnapshoot.java @@ -0,0 +1,159 @@ +package fastdex.build.lib.snapshoot.sourceset; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.api.Snapshoot; +import fastdex.build.lib.snapshoot.api.Status; +import fastdex.build.lib.snapshoot.file.FileNode; +import fastdex.build.lib.snapshoot.string.BaseStringSnapshoot; +import fastdex.build.lib.snapshoot.string.StringDiffInfo; +import fastdex.build.lib.snapshoot.string.StringNode; +import com.google.gson.annotations.SerializedName; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by tong on 17/3/31. + */ +public final class SourceSetSnapshoot extends BaseStringSnapshoot { + public String path;//工程目录 + + @SerializedName("sourceSets") + public Set directorySnapshootSet = new HashSet<>(); + + public SourceSetSnapshoot() { + } + + public SourceSetSnapshoot(SourceSetSnapshoot snapshoot) { + super(snapshoot); + //from gson + this.path = snapshoot.path; + this.directorySnapshootSet.addAll(snapshoot.directorySnapshootSet); + } + + public SourceSetSnapshoot(File projectDir, Set sourceSets) throws IOException { + super(SourceSetSnapshoot.getSourceSetStringArray(sourceSets)); + init(projectDir,sourceSets); + } + + public SourceSetSnapshoot(File projectDir, String ...sourceSets) throws IOException { + super(sourceSets); + + Set result = new HashSet<>(); + if (sourceSets != null) { + for (String string : sourceSets) { + result.add(new File(string)); + } + } + init(projectDir,result); + } + + private void init(File projectDir,Set sourceSetFiles) throws IOException { + if (projectDir == null || projectDir.length() == 0) { + throw new RuntimeException("Invalid projectPath"); + } + this.path = projectDir.getAbsolutePath(); + if (directorySnapshootSet == null) { + directorySnapshootSet = new HashSet<>(); + } + + if (sourceSetFiles != null) { + for (File sourceSet : sourceSetFiles) { + if (sourceSet != null) { + JavaDirectorySnapshoot javaDirectorySnapshoot = new JavaDirectorySnapshoot(sourceSet); + javaDirectorySnapshoot.projectPath = projectDir.getAbsolutePath(); + directorySnapshootSet.add(javaDirectorySnapshoot); + } + } + } + } + + public void addJavaDirectorySnapshoot(JavaDirectorySnapshoot javaDirectorySnapshoot) { + nodes.add(StringNode.create(javaDirectorySnapshoot.path)); + directorySnapshootSet.add(javaDirectorySnapshoot); + } + + @Override + protected SourceSetDiffResultSet createEmptyResultSet() { + return new SourceSetDiffResultSet(); + } + + @Override + public DiffResultSet diff(Snapshoot otherSnapshoot) { + SourceSetDiffResultSet sourceSetResultSet = (SourceSetDiffResultSet) super.diff(otherSnapshoot); + + SourceSetSnapshoot oldSnapshoot = (SourceSetSnapshoot)otherSnapshoot; + for (DiffInfo diffInfo : sourceSetResultSet.getDiffInfos(Status.DELETEED)) { + JavaDirectorySnapshoot javaDirectorySnapshoot = oldSnapshoot.getJavaDirectorySnapshootByPath(diffInfo.uniqueKey); + + JavaDirectoryDiffResultSet javaDirectoryDiffResultSet = javaDirectorySnapshoot.createEmptyResultSet(); + for (FileNode node : javaDirectorySnapshoot.nodes) { + javaDirectoryDiffResultSet.add(new JavaFileDiffInfo(Status.DELETEED,null,node)); + //sourceSetResultSet.addJavaFileDiffInfo(new JavaFileDiffInfo(Status.DELETEED,null,node)); + } + sourceSetResultSet.mergeJavaDirectoryResultSet(path,javaDirectoryDiffResultSet); + } + + for (DiffInfo diffInfo : sourceSetResultSet.getDiffInfos(Status.ADDED)) { + JavaDirectorySnapshoot javaDirectorySnapshoot = getJavaDirectorySnapshootByPath(diffInfo.uniqueKey); + + JavaDirectoryDiffResultSet javaDirectoryDiffResultSet = javaDirectorySnapshoot.createEmptyResultSet(); + for (FileNode node : javaDirectorySnapshoot.nodes) { + javaDirectoryDiffResultSet.add(new JavaFileDiffInfo(Status.ADDED,node,null)); + //sourceSetResultSet.addJavaFileDiffInfo(new JavaFileDiffInfo(Status.ADDED,node,null)); + } + sourceSetResultSet.mergeJavaDirectoryResultSet(path,javaDirectoryDiffResultSet); + } + + for (DiffInfo diffInfo : sourceSetResultSet.getDiffInfos(Status.NOCHANGED)) { + JavaDirectorySnapshoot now = getJavaDirectorySnapshootByPath(diffInfo.uniqueKey); + JavaDirectorySnapshoot old = oldSnapshoot.getJavaDirectorySnapshootByPath(diffInfo.uniqueKey); + + JavaDirectoryDiffResultSet resultSet = (JavaDirectoryDiffResultSet) now.diff(old); + sourceSetResultSet.mergeJavaDirectoryResultSet(now.path,resultSet); + } + + return sourceSetResultSet; + } + + private JavaDirectorySnapshoot getJavaDirectorySnapshootByPath(String path) { + for (JavaDirectorySnapshoot snapshoot : directorySnapshootSet) { + if (snapshoot.path.equals(path)) { + return snapshoot; + } + } + return null; + } + +// public void applyNewProjectDir(String oldRootProjectPath,String curRootProjectPath,String curProjectPath) { +// this.path = curProjectPath; +// +// for (StringNode node : nodes) { +// node.setString(node.getString().replaceAll(oldRootProjectPath,curRootProjectPath)); +// } +// for (JavaDirectorySnapshoot snapshoot : directorySnapshootSet) { +// snapshoot.path = snapshoot.path.replaceAll(oldRootProjectPath,curRootProjectPath); +// snapshoot.projectPath = snapshoot.projectPath.replaceAll(oldRootProjectPath,curRootProjectPath); +// } +// } + + @Override + public String toString() { + return "SourceSetSnapshoot{" + + "path='" + path + '\'' + + ", directorySnapshootSet=" + directorySnapshootSet + + '}'; + } + + public static Set getSourceSetStringArray(Set sourceSets) { + Set result = new HashSet<>(); + if (sourceSets != null) { + for (File file : sourceSets) { + result.add(file.getAbsolutePath()); + } + } + return result; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/BaseStringSnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/BaseStringSnapshoot.java new file mode 100644 index 00000000..3803820f --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/BaseStringSnapshoot.java @@ -0,0 +1,45 @@ +package fastdex.build.lib.snapshoot.string; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.api.Snapshoot; +import fastdex.build.lib.snapshoot.api.Status; + +import java.io.IOException; +import java.util.Set; + +/** + * Created by tong on 17/3/31. + */ +public class BaseStringSnapshoot extends Snapshoot { + + public BaseStringSnapshoot() { + } + + public BaseStringSnapshoot(BaseStringSnapshoot snapshoot) { + super(snapshoot); + } + + public BaseStringSnapshoot(Set strings) throws IOException { + for (String str : strings) { + addNode((NODE) StringNode.create(str)); + } + } + + public BaseStringSnapshoot(String ...strings) throws IOException { + for (String str : strings) { + addNode((NODE) StringNode.create(str)); + } + } + + @Override + protected DiffInfo createEmptyDiffInfo() { + return new StringDiffInfo(); + } + + @Override + protected void diffNode(DiffResultSet diffInfos, Snapshoot otherSnapshoot, NODE now, NODE old) { + //不需要对比变化 + addDiffInfo(diffInfos,createDiffInfo(Status.NOCHANGED,now,old)); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringDiffInfo.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringDiffInfo.java new file mode 100644 index 00000000..aae1b9cd --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringDiffInfo.java @@ -0,0 +1,16 @@ +package fastdex.build.lib.snapshoot.string; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.Status; + +/** + * Created by tong on 17/3/31. + */ +public class StringDiffInfo extends DiffInfo { + public StringDiffInfo() { + } + + public StringDiffInfo(Status status, StringNode now, StringNode old) { + super(status, (now != null ? now.getUniqueKey() : old.getUniqueKey()), now, old); + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringNode.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringNode.java new file mode 100644 index 00000000..3bc4bb75 --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringNode.java @@ -0,0 +1,41 @@ +package fastdex.build.lib.snapshoot.string; + +import fastdex.build.lib.snapshoot.api.Node; + +/** + * Created by tong on 17/3/31. + */ +public class StringNode extends Node { + private String string; + + public StringNode() { + } + + public StringNode(String string) { + this.string = string; + } + + public void setString(String string) { + this.string = string; + } + + public String getString() { + return string; + } + + @Override + public String getUniqueKey() { + return string; + } + + public static StringNode create(String string) { + return new StringNode(string); + } + + @Override + public String toString() { + return "StringNode{" + + "string='" + string + '\'' + + '}'; + } +} diff --git a/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringSnapshoot.java b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringSnapshoot.java new file mode 100644 index 00000000..9ca0e9da --- /dev/null +++ b/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot/string/StringSnapshoot.java @@ -0,0 +1,24 @@ +package fastdex.build.lib.snapshoot.string; + +import java.io.IOException; +import java.util.Set; + +/** + * Created by tong on 17/3/31. + */ +public final class StringSnapshoot extends BaseStringSnapshoot { + public StringSnapshoot() { + } + + public StringSnapshoot(StringSnapshoot snapshoot) { + super(snapshoot); + } + + public StringSnapshoot(Set strings) throws IOException { + super(strings); + } + + public StringSnapshoot(String... strings) throws IOException { + super(strings); + } +} diff --git a/fastdex-build-lib/src/test/java/snapshoot/BuildConfigAndRDiffTest.java b/fastdex-build-lib/src/test/java/snapshoot/BuildConfigAndRDiffTest.java new file mode 100644 index 00000000..0847e1a7 --- /dev/null +++ b/fastdex-build-lib/src/test/java/snapshoot/BuildConfigAndRDiffTest.java @@ -0,0 +1,60 @@ +package snapshoot; + +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.file.FileSuffixFilter; +import fastdex.build.lib.snapshoot.file.ScanFilter; +import fastdex.build.lib.snapshoot.sourceset.JavaDirectorySnapshoot; +import fastdex.build.lib.snapshoot.sourceset.SourceSetSnapshoot; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import junit.framework.TestCase; +import org.junit.Test; + +/** + * Created by tong on 17/3/31. + */ +public class BuildConfigAndRDiffTest extends TestCase { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + @Test + public void test() throws Throwable { +// SourceSetSnapshoot sourceSetSnapshoot = new SourceSetSnapshoot(new File("/Users/tong/Projects/fastdex/sample/app"),"/Users/tong/Projects/fastdex/sample/app/src/main/java"); +// +// final File rDir = new File("/Users/tong/Projects/fastdex/sample/app/build/generated/source/r/debug"); +// JavaDirectorySnapshoot rSnapshoot = new JavaDirectorySnapshoot(rDir){ +// @Override +// protected void walkFileTree(File directory, ScanFilter scanFilter) throws IOException { +// visitFile(new File("/Users/tong/Projects/fastdex/sample/app/build/generated/source/r/debug/com/dx168/fastdex/sample/R.java").toPath(),null,scanFilter); +// } +// }; +// +// File buildConfigDir = new File("/Users/tong/Projects/fastdex/sample/app/build/generated/source/buildConfig/debug"); +// JavaDirectorySnapshoot buildConfigSnapshoot = new JavaDirectorySnapshoot(buildConfigDir){ +// @Override +// protected void walkFileTree(File directory, ScanFilter scanFilter) throws IOException { +// visitFile(new File("/Users/tong/Projects/fastdex/sample/app/build/generated/source/buildConfig/debug/com/dx168/fastdex/sample/BuildConfig.java").toPath(),null,scanFilter); +// } +// }; +// sourceSetSnapshoot.addJavaDirectorySnapshoot(rSnapshoot); +// sourceSetSnapshoot.addJavaDirectorySnapshoot(buildConfigSnapshoot); +// +// SourceSetSnapshoot oldSourceSetSnapshoot = (SourceSetSnapshoot) SourceSetSnapshoot.load(new File("/Users/tong/Projects/fastdex/sample/app/build/fastdex/Debug/sourceSets.json"),SourceSetSnapshoot.class); +// DiffResultSet diffResultSet = sourceSetSnapshoot.diff(oldSourceSetSnapshoot); +// +// System.out.println(diffResultSet); +// DiffResultSet diffResultSet2 = sourceSetSnapshoot.diff(oldSourceSetSnapshoot); + } + + + @Test + public void test2() throws Throwable { +// JavaDirectorySnapshoot snapshoot = new JavaDirectorySnapshoot(new File("/Users/tong/Projects/fastdex/sample/app/build/intermediates/classes/debug"),new FileSuffixFilter(".class")); +// JavaDirectorySnapshoot oldSnapshoot = (JavaDirectorySnapshoot) JavaDirectorySnapshoot.load(new File("/Users/tong/Desktop/snapshoot.json"),JavaDirectorySnapshoot.class); +// +// DiffResultSet diffResultSet = snapshoot.diff(oldSnapshoot); +// System.out.println(GSON.toJson(diffResultSet.changedDiffInfos)); +// +// snapshoot.serializeTo(new FileOutputStream("/Users/tong/Desktop/snapshoot.json")); + } +} diff --git a/fastdex-build-lib/src/test/java/snapshoot/ResultSetTest.java b/fastdex-build-lib/src/test/java/snapshoot/ResultSetTest.java new file mode 100644 index 00000000..e45f67c8 --- /dev/null +++ b/fastdex-build-lib/src/test/java/snapshoot/ResultSetTest.java @@ -0,0 +1,7 @@ +package snapshoot; + +/** + * Created by tong on 17/3/31. + */ +public class ResultSetTest { +} diff --git a/fastdex-build-lib/src/test/java/snapshoot/SourceSetSnapshootTest.java b/fastdex-build-lib/src/test/java/snapshoot/SourceSetSnapshootTest.java new file mode 100644 index 00000000..d643b6c8 --- /dev/null +++ b/fastdex-build-lib/src/test/java/snapshoot/SourceSetSnapshootTest.java @@ -0,0 +1,134 @@ +package snapshoot; + +import fastdex.build.lib.snapshoot.file.FileNode; +import fastdex.build.lib.snapshoot.sourceset.JavaDirectorySnapshoot; +import fastdex.build.lib.snapshoot.sourceset.SourceSetDiffResultSet; +import fastdex.build.lib.snapshoot.sourceset.SourceSetSnapshoot; +import junit.framework.TestCase; +import org.junit.Test; +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; + +/** + * Created by tong on 17/3/31. + */ +public class SourceSetSnapshootTest extends TestCase { + String workDir; + String source_set1; + String source_set2; + String source_set11; + String source_set22; + + @Override + protected void setUp() throws Exception { + super.setUp(); + File currentPath = new File(this.getClass().getResource("/").getPath()); + System.out.println(currentPath); + + workDir = "/Users/tong/Desktop/sourceSetTest"; + source_set1 = workDir + File.separator + "source_set1"; + source_set2 = workDir + File.separator + "source_set2"; + source_set11 = workDir + File.separator + "source_set11"; + source_set22 = workDir + File.separator + "source_set22"; + } + + @Test + public void testCreate() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + + SourceSetSnapshoot snapshoot = new SourceSetSnapshoot(new File(workDir),source_set1,source_set2); + assertEquals(snapshoot.directorySnapshootSet.size(),2); + SourceSetSnapshoot snapshoot2 = new SourceSetSnapshoot(new File(workDir),source_set1,source_set1); + assertEquals(snapshoot2.directorySnapshootSet.size(),1); + } + + @Test + public void testDiffAddOneSourceSet() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + SourceSetSnapshoot now = new SourceSetSnapshoot(new File(workDir),source_set1,source_set2); + SourceSetSnapshoot old = new SourceSetSnapshoot(new File(workDir),source_set1); + + SourceSetDiffResultSet sourceSetResultSet = (SourceSetDiffResultSet) now.diff(old); + assertTrue(sourceSetResultSet.isJavaFileChanged()); + + + System.out.println(sourceSetResultSet); + } + + @Test + public void testSave() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + SourceSetSnapshoot now = new SourceSetSnapshoot(new File(workDir),source_set1,source_set2); + now.serializeTo(new FileOutputStream(new File(workDir,"now.json"))); + } + + @Test + public void testDiff1() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + + SourceSetSnapshoot now = new SourceSetSnapshoot(new File(workDir),source_set1); + SourceSetSnapshoot old = new SourceSetSnapshoot(new File(workDir),source_set11); + + SourceSetDiffResultSet sourceSetResultSet = (SourceSetDiffResultSet) now.diff(old); + System.out.println(sourceSetResultSet.toString()); + sourceSetResultSet.serializeTo(new FileOutputStream(new File(workDir,"diff.json"))); + } + + @Test + public void testDiff2() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + + SourceSetSnapshoot now = new SourceSetSnapshoot(new File(workDir),source_set1); + now.serializeTo(new FileOutputStream(new File(workDir,"snapshoot.json"))); + + SourceSetSnapshoot old = (SourceSetSnapshoot) SourceSetSnapshoot.load(new File(workDir,"snapshoot.json"),SourceSetSnapshoot.class); + JavaDirectorySnapshoot javaDirectorySnapshoot = new ArrayList<>(old.directorySnapshootSet).get(0); + FileNode fileNode = new ArrayList<>(javaDirectorySnapshoot.nodes).get(0); + fileNode.lastModified = System.currentTimeMillis(); + + SourceSetDiffResultSet resultSet = (SourceSetDiffResultSet) now.diff(old); + + assertEquals(resultSet.changedJavaFileDiffInfos.size(),1); + System.out.println(resultSet); + } + + @Test + public void testDiff3() throws Throwable { + if (!isDir(source_set1) || !isDir(source_set2) || !isDir(source_set11) || !isDir(source_set22)) { + System.err.println("Test-env not init!!"); + } + + SourceSetSnapshoot now = new SourceSetSnapshoot(new File("/Users/tong/Projects/fastdex/DevSample/app"),""); + } + + + public boolean isDir(File dir) { + if (dir == null) { + return false; + } + + if (!dir.exists() || !dir.isDirectory()) { + return false; + } + + return true; + } + + public boolean isDir(String dir) { + if (dir == null) { + return false; + } + return isDir(new File(dir)); + } +} diff --git a/fastdex-build-lib/src/test/java/snapshoot/StringSnapshootTest.java b/fastdex-build-lib/src/test/java/snapshoot/StringSnapshootTest.java new file mode 100644 index 00000000..236ff16b --- /dev/null +++ b/fastdex-build-lib/src/test/java/snapshoot/StringSnapshootTest.java @@ -0,0 +1,122 @@ +package snapshoot; + +import fastdex.build.lib.snapshoot.api.DiffInfo; +import fastdex.build.lib.snapshoot.api.DiffResultSet; +import fastdex.build.lib.snapshoot.api.Node; +import fastdex.build.lib.snapshoot.api.Status; +import fastdex.build.lib.snapshoot.string.StringDiffInfo; +import fastdex.build.lib.snapshoot.string.StringNode; +import fastdex.build.lib.snapshoot.string.StringSnapshoot; +import junit.framework.TestCase; +import org.junit.Test; +import java.util.*; + +/** + * Created by tong on 17/3/31. + */ +public class StringSnapshootTest extends TestCase { + @Test + public void testCreate() throws Throwable { + Set nodeSet = new HashSet<>(); + nodeSet.add("a"); + nodeSet.add("b"); + nodeSet.add("c"); + + StringSnapshoot snapshoot1 = new StringSnapshoot(nodeSet); + Collection collection = (Collection) snapshoot1.nodes; + assertEquals(collection.size(),nodeSet.size()); + Set strings = new HashSet<>(); + + for (Node node : collection) { + strings.add(node.getUniqueKey()); + } + + assertEquals(strings,nodeSet); + } + + @Test + public void testEqual() throws Throwable { + Set nodeSet = new HashSet<>(); + nodeSet.add("/Users/tong/Projects/fastdex/DevSample/app/src/main/java"); + nodeSet.add("/Users/tong/Projects/fastdex/DevSample/app/src/main/java1"); + nodeSet.add("/Users/tong/Projects/fastdex/DevSample/app/src/main/java2"); + + + StringSnapshoot snapshoot1 = new StringSnapshoot(nodeSet); + StringSnapshoot snapshoot2 = new StringSnapshoot(nodeSet); + + DiffResultSet resultSet = snapshoot1.diff(snapshoot2); + + assertTrue(resultSet.getAllChangedDiffInfos() == null || resultSet.getAllChangedDiffInfos().isEmpty()); + } + + @Test + public void testEqual2() throws Throwable { + Collection nodes1 = new HashSet<>(); + nodes1.add(StringNode.create("/Users/tong/Projects/fastdex/DevSample/app/src/main/java")); + + Collection nodes2 = new HashSet<>(); + nodes2.add(StringNode.create("/Users/tong/Projects/fastdex/DevSample/app/src/main/java")); + + Set increasedNodes = new HashSet<>(); + increasedNodes.addAll(nodes1); + increasedNodes.removeAll(nodes2); + + System.out.println(increasedNodes); + + assertTrue(increasedNodes.isEmpty()); + } + + @Test + public void testAdd() throws Throwable { + Set nodeSet = new HashSet<>(); + nodeSet.add("a"); + nodeSet.add("b"); + nodeSet.add("c"); + StringSnapshoot now = new StringSnapshoot(nodeSet); + + Set nodeSet2 = new HashSet<>(); + nodeSet2.add("a"); + nodeSet2.add("b"); + StringSnapshoot old = new StringSnapshoot(nodeSet2); + + DiffResultSet resultSet = now.diff(old); + + assertEquals(resultSet.getAllChangedDiffInfos().size(),1); + + + ArrayList ss = new ArrayList<>(); + ss.addAll(resultSet.getAllChangedDiffInfos()); + DiffInfo diffInfo = ss.get(0); + + assertEquals(diffInfo.status, Status.ADDED); + assertEquals(diffInfo.uniqueKey, "c"); + } + + @Test + public void testDelete() throws Throwable { + Set nodeSet = new HashSet<>(); + nodeSet.add("a"); + nodeSet.add("b"); + + StringSnapshoot now = new StringSnapshoot(nodeSet); + + Set nodeSet2 = new HashSet<>(); + nodeSet2.add("a"); + nodeSet2.add("b"); + nodeSet2.add("c"); + StringSnapshoot old = new StringSnapshoot(nodeSet2); + + DiffResultSet resultSet = now.diff(old); + + assertEquals(resultSet.getAllChangedDiffInfos().size(),1); + + + ArrayList ss = new ArrayList<>(); + ss.addAll(resultSet.getAllChangedDiffInfos()); + DiffInfo diffInfo = ss.get(0); + + assertEquals(diffInfo.status, Status.DELETEED); + assertEquals(diffInfo.uniqueKey, "c"); + } +} diff --git a/fastdex-common/.gitignore b/fastdex-common/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/fastdex-common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/fastdex-common/build.gradle b/fastdex-common/build.gradle new file mode 100644 index 00000000..b5708cbe --- /dev/null +++ b/fastdex-common/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'java' +apply plugin: 'maven' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +[compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.google.code.gson:gson:2.3.1' + + testCompile 'junit:junit:4.12' + testCompile 'com.google.code.gson:gson:2.3.1' +} + +apply from: rootProject.file('bintray.gradle') diff --git a/fastdex-common/src/main/java/fastdex/common/ShareConstants.java b/fastdex-common/src/main/java/fastdex/common/ShareConstants.java new file mode 100644 index 00000000..6b9206d5 --- /dev/null +++ b/fastdex-common/src/main/java/fastdex/common/ShareConstants.java @@ -0,0 +1,16 @@ +package fastdex.common; + +/** + * Created by tong on 17/4/28. + */ +public interface ShareConstants { + String JAVA_SUFFIX = ".java"; + String CLASS_SUFFIX = ".class"; + String DEX_SUFFIX = ".dex"; + String CLASSES = "classes"; + String CLASSES_DEX = CLASSES + DEX_SUFFIX; + String META_INFO_FILENAME = "fastdex-meta-info.json"; + String RESOURCE_APK_FILE_NAME = "resources.apk"; + String MERGED_PATCH_DEX = "merged-patch.dex"; + String PATCH_DEX = "patch.dex"; +} diff --git a/fastdex-common/src/main/java/fastdex/common/fd/ProtocolConstants.java b/fastdex-common/src/main/java/fastdex/common/fd/ProtocolConstants.java new file mode 100644 index 00000000..22cf462f --- /dev/null +++ b/fastdex-common/src/main/java/fastdex/common/fd/ProtocolConstants.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.common.fd; + +/** + * Constants shared between Android Studio and the Instant Run runtime. + */ +public interface ProtocolConstants { + + /** + * Magic (random) number used to identify the protocol + */ + long PROTOCOL_IDENTIFIER = 0x35107124L; + + /** + * Version of the protocol + */ + int PROTOCOL_VERSION = 1; + + /** + * Message: sending patches + */ + int MESSAGE_PATCHES = 1; + + /** + * Message: ping, send ack back + */ + int MESSAGE_PING = 2; + + /** + * Message: look up a very quick checksum of the given path; this + * may not pick up on edits in the middle of the file but should be a + * quick way to determine if a path exists and some basic information + * about it. + *

+ * Currently disabled.. Tied to using extracted resource + * directories (controlled by FileManager#USE_EXTRACTED_RESOURCES). + */ + int MESSAGE_PATH_EXISTS = 3; + + /** + * Message: query whether the app has a given file and if so return + * its checksum. (This is used to determine whether the app can receive + * a small delta on top of a (typically resource ) file instead of resending the whole + * file over again.) + *

+ * Currently disabled.. Tied to using extracted resource + * directories (controlled by FileManager#USE_EXTRACTED_RESOURCES). + */ + int MESSAGE_PATH_CHECKSUM = 4; + + /** + * Message: restart activities + */ + int MESSAGE_RESTART_ACTIVITY = 5; + + /** + * Message: show toast + */ + int MESSAGE_SHOW_TOAST = 6; + + /** + * Done transmitting + */ + int MESSAGE_EOF = 7; + + /** + * Message: ask the run-as server to copy a file as the app userid. + */ + int MESSAGE_SEND_FILE = 8; + + /** + * Message: ask the run-as server to execute a shell command as the app userid. + */ + int MESSAGE_SHELL_COMMAND = 9; + + /** + * No updates + */ + int UPDATE_MODE_NONE = 0; + + /** + * Patch changes directly, keep app running without any restarting + */ + int UPDATE_MODE_HOT_SWAP = 1; + + /** + * Patch changes, restart activity to reflect changes + */ + int UPDATE_MODE_WARM_SWAP = 2; + + /** + * Store change in app directory, restart app + */ + int UPDATE_MODE_COLD_SWAP = 3; +} diff --git a/fastdex-common/src/main/java/fastdex/common/utils/FileUtils.java b/fastdex-common/src/main/java/fastdex/common/utils/FileUtils.java new file mode 100644 index 00000000..63c2eae9 --- /dev/null +++ b/fastdex-common/src/main/java/fastdex/common/utils/FileUtils.java @@ -0,0 +1,294 @@ +package fastdex.common.utils; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import fastdex.common.ShareConstants; + +/** + * Created by tong on 17/3/10. + */ +public class FileUtils { + public static final int BUFFER_SIZE = 16384; + + public static final boolean ensumeDir(File file) { + if (file == null) { + return false; + } + if (!fileExists(file.getAbsolutePath())) { + return file.mkdirs(); + } + return true; + } + + public static final boolean fileExists(String filePath) { + if (filePath == null) { + return false; + } + + File file = new File(filePath); + if (file.exists() && file.isFile()) { + return true; + } + return false; + } + + public static final boolean dirExists(String filePath) { + if (filePath == null) { + return false; + } + + File file = new File(filePath); + if (file.exists() && file.isDirectory()) { + return true; + } + return false; + } + + public static final boolean deleteFile(String filePath) { + if (filePath == null) { + return true; + } + + File file = new File(filePath); + if (file.exists()) { + return file.delete(); + } + return true; + } + + public static final boolean deleteFile(File file) { + if (file == null) { + return true; + } + if (file.exists()) { + return file.delete(); + } + return true; + } + + public static boolean isLegalFile(File file) { + if (file == null) { + return false; + } + return file.exists() && file.isFile() && file.length() > 0; + } + + public static long getFileSizes(File f) { + if (f == null) { + return 0; + } + long size = 0; + if (f.exists() && f.isFile()) { + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + size = fis.available(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (fis != null) { + fis.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return size; + } + + public static final boolean deleteDir(File file) { + if (file == null || (!file.exists())) { + return false; + } + if (file.isFile()) { + file.delete(); + } else if (file.isDirectory()) { + File[] files = file.listFiles(); + for (int i = 0; i < files.length; i++) { + deleteDir(files[i]); + } + } + file.delete(); + return true; + } + + public static void cleanDir(File dir) { + if (dir.exists()) { + FileUtils.deleteDir(dir); + dir.mkdirs(); + } + } + + public static void copyResourceUsingStream(String name, File dest) throws IOException { + FileOutputStream os = null; + File parent = dest.getParentFile(); + if (parent != null && (!parent.exists())) { + parent.mkdirs(); + } + InputStream is = null; + + try { + is = FileUtils.class.getResourceAsStream("/" + name); + os = new FileOutputStream(dest, false); + + byte[] buffer = new byte[BUFFER_SIZE]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + } finally { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + } + } + + public static void copyFileUsingStream(File source, File dest) throws IOException { + FileInputStream is = null; + FileOutputStream os = null; + File parent = dest.getParentFile(); + if (parent != null && (!parent.exists())) { + parent.mkdirs(); + } + try { + is = new FileInputStream(source); + os = new FileOutputStream(dest, false); + + byte[] buffer = new byte[BUFFER_SIZE]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + dest.setLastModified(source.lastModified()); + } finally { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + } + } + + public static void write2file(byte[] content, File dest) throws IOException { + FileOutputStream os = null; + File parent = dest.getParentFile(); + if (parent != null && (!parent.exists())) { + parent.mkdirs(); + } + try { + os = new FileOutputStream(dest, false); + os.write(content); + } finally { + if (os != null) { + os.close(); + } + } + } + + public static byte[] readContents(final File file) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final int bufferSize = BUFFER_SIZE; + try { + final FileInputStream fis = new FileInputStream(file); + final BufferedInputStream bIn = new BufferedInputStream(fis); + int length; + byte[] buffer = new byte[bufferSize]; + byte[] bufferCopy; + while ((length = bIn.read(buffer, 0, bufferSize)) != -1) { + bufferCopy = new byte[length]; + System.arraycopy(buffer, 0, bufferCopy, 0, length); + output.write(bufferCopy); + } + bIn.close(); + } finally { + output.close(); + } + return output.toByteArray(); + } + + public static byte[] readStream(final InputStream is) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final int bufferSize = BUFFER_SIZE; + try { + final BufferedInputStream bIn = new BufferedInputStream(is); + int length; + byte[] buffer = new byte[bufferSize]; + byte[] bufferCopy; + while ((length = bIn.read(buffer, 0, bufferSize)) != -1) { + bufferCopy = new byte[length]; + System.arraycopy(buffer, 0, bufferCopy, 0, length); + output.write(bufferCopy); + } + bIn.close(); + } finally { + output.close(); + is.close(); + } + return output.toByteArray(); + } + + public static final void copyDir(File sourceDir, File destDir, final String suffix) throws IOException { + final Path sourcePath = sourceDir.toPath(); + final Path destPath = destDir.toPath(); + Files.walkFileTree(sourceDir.toPath(),new SimpleFileVisitor(){ + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (suffix != null && !file.toFile().getName().endsWith(suffix)) { + return FileVisitResult.CONTINUE; + } + Path relativePath = sourcePath.relativize(file); + Path classFilePath = destPath.resolve(relativePath); + + File source = file.toFile(); + File dest = classFilePath.toFile(); + copyFileUsingStream(source,dest); + + dest.setLastModified(source.lastModified()); + return FileVisitResult.CONTINUE; + } + }); + } + + public static final void copyDir(File sourceDir, File destDir) throws IOException { + copyDir(sourceDir,destDir,null); + } + + + /** + * 目录中是否存在dex + * @param dir + * @return + */ + public static boolean hasDex(File dir) { + if (!dirExists(dir.getAbsolutePath())) { + return false; + } + + //check dex + boolean result = false; + for (File file : dir.listFiles()) { + if (file.getName().endsWith(ShareConstants.DEX_SUFFIX)) { + result = true; + break; + } + } + return result; + } +} diff --git a/fastdex-common/src/main/java/fastdex/common/utils/SerializeUtils.java b/fastdex-common/src/main/java/fastdex/common/utils/SerializeUtils.java new file mode 100644 index 00000000..44a81cd2 --- /dev/null +++ b/fastdex-common/src/main/java/fastdex/common/utils/SerializeUtils.java @@ -0,0 +1,44 @@ +package fastdex.common.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.*; + +/** + * Created by tong on 17/3/30. + */ +public class SerializeUtils { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public static T load(InputStream inputStream,Class type) throws IOException { + String json = new String(FileUtils.readStream(inputStream)); + return GSON.fromJson(json,type); + } + + public static void serializeTo(OutputStream outputStream,Object obj) throws IOException { + String json = GSON.toJson(obj); + try { + outputStream.write(json.getBytes()); + outputStream.flush(); + } finally { + if (outputStream != null) { + outputStream.close(); + } + } + } + + public static void serializeTo(File file,Object obj) throws IOException { + String json = GSON.toJson(obj); + FileOutputStream outputStream = null; + FileUtils.ensumeDir(file.getParentFile()); + try { + outputStream = new FileOutputStream(file); + outputStream.write(json.getBytes()); + outputStream.flush(); + } finally { + if (outputStream != null) { + outputStream.close(); + } + } + } +} diff --git a/fastdex-gradle/.gitignore b/fastdex-gradle/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/fastdex-gradle/.gitignore @@ -0,0 +1 @@ +/build diff --git a/fastdex-gradle/build.gradle b/fastdex-gradle/build.gradle new file mode 100644 index 00000000..6924b078 --- /dev/null +++ b/fastdex-gradle/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'groovy' +apply plugin: 'maven' + +dependencies { + compile gradleApi() + compile localGroovy() + compile project(':fastdex-build-lib') + compile 'com.android.tools.build:gradle:2.1.2' + + //compile project(':instant-run:instant-run-client') +} + +repositories { + jcenter() +} + +apply from: rootProject.file('bintray.gradle') \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/FastdexPlugin.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/FastdexPlugin.groovy new file mode 100644 index 00000000..757c9491 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/FastdexPlugin.groovy @@ -0,0 +1,259 @@ +package com.dx168.fastdex.build + +import com.android.build.api.transform.Transform +import com.android.build.gradle.internal.pipeline.TransformTask +import com.android.build.gradle.internal.transforms.DexTransform +import com.android.build.gradle.internal.transforms.JarMergingTransform +import com.dx168.fastdex.build.task.FastdexCleanTask +import com.dx168.fastdex.build.task.FastdexCreateMaindexlistFileTask +import com.dx168.fastdex.build.task.FastdexInstantRunTask +import com.dx168.fastdex.build.task.FastdexManifestTask +import com.dx168.fastdex.build.task.FastdexResourceIdTask +import com.dx168.fastdex.build.transform.FastdexJarMergingTransform +import com.dx168.fastdex.build.util.FastdexBuildListener +import com.dx168.fastdex.build.util.Constants +import com.dx168.fastdex.build.util.GradleUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownTaskException +import org.gradle.api.execution.TaskExecutionGraph +import org.gradle.api.execution.TaskExecutionGraphListener +import java.lang.reflect.Field +import com.dx168.fastdex.build.transform.FastdexTransform +import com.dx168.fastdex.build.extension.FastdexExtension +import com.dx168.fastdex.build.task.FastdexPrepareTask +import com.dx168.fastdex.build.task.FastdexCustomJavacTask + +/** + * 注册相应节点的任务 + * Created by tong on 17/10/3. + */ +class FastdexPlugin implements Plugin { + @Override + public void apply(Project project) { + project.extensions.create('fastdex', FastdexExtension) + + FastdexBuildListener.addByProject(project) + project.afterEvaluate { + def configuration = project.fastdex + if (!configuration.fastdexEnable) { + project.logger.error("====fastdex tasks are disabled.====") + return + } + if (!project.plugins.hasPlugin('com.android.application')) { + throw new GradleException('generateTinkerApk: Android Application plugin required') + } + + //最低支持2.0.0 + String androidGradlePluginVersion = GradleUtils.ANDROID_GRADLE_PLUGIN_VERSION + if (androidGradlePluginVersion.compareTo(Constants.MIN_SUPPORT_ANDROID_GRADLE_VERSION) < 0) { + throw new GradleException("Your version too old 'com.android.tools.build:gradle:${androidGradlePluginVersion}', minimum support version 2.0.0") + } + + def android = project.extensions.android + //open jumboMode + android.dexOptions.jumboMode = true + //close preDexLibraries + try { + android.dexOptions.preDexLibraries = false + } catch (Throwable e) { + //no preDexLibraries field, just continue + } + + project.tasks.create("fastdexCleanAll", FastdexCleanTask) + + android.applicationVariants.all { variant -> + def variantOutput = variant.outputs.first() + def variantName = variant.name.capitalize() + + try { + //与instant run有冲突需要禁掉instant run + def instantRunTask = project.tasks.getByName("transformClassesWithInstantRunFor${variantName}") + if (instantRunTask) { + throw new GradleException( + "Fastdex does not support instant run mode, please trigger build" + + " by assemble${variantName} or disable instant run" + + " in 'File->Settings...'." + ) + } + } catch (UnknownTaskException e) { + // Not in instant run mode, continue. + } + FastdexVariant fastdexVariant = new FastdexVariant(project,variant) + + boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled + //TODO 暂时忽略开启混淆的buildType(目前的快照对比方案 无法映射java文件的类名和混淆后的class的类名) + if (proguardEnable) { + String buildTypeName = variant.getBuildType().buildType.getName() + project.logger.error("--------------------fastdex--------------------") + project.logger.error("fastdex android.buildTypes.${buildTypeName}.minifyEnabled=true, just ignore") + project.logger.error("--------------------fastdex--------------------") + } + else { + //创建清理指定variantName缓存的任务(用户触发) + FastdexCleanTask cleanTask = project.tasks.create("fastdexCleanFor${variantName}", FastdexCleanTask) + cleanTask.fastdexVariant = fastdexVariant + + //fix issue#8 + def tinkerPatchManifestTask = getTinkerPatchManifestTask(project, variantName) + if (tinkerPatchManifestTask != null) { + manifestTask.mustRunAfter tinkerPatchManifestTask + } + + //TODO change api + variantOutput.processManifest.dependsOn getMergeDebugResources(project,variantName) + //variantOutput.processManifest.dependsOn variant.getVariantData().getScope().getMergeResourcesTask() + //替换项目的Application为com.dx168.fastdex.runtime.FastdexApplication + FastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask) + manifestTask.fastdexVariant = fastdexVariant + manifestTask.mustRunAfter variantOutput.processManifest + variantOutput.processResources.dependsOn manifestTask + + //保持补丁打包时R文件中相同的节点和第一次打包时的值保持一致 + FastdexResourceIdTask applyResourceTask = project.tasks.create("fastdexProcess${variantName}ResourceId", FastdexResourceIdTask) + applyResourceTask.fastdexVariant = fastdexVariant + applyResourceTask.resDir = variantOutput.processResources.resDir + //let applyResourceTask run after manifestTask + applyResourceTask.mustRunAfter manifestTask + variantOutput.processResources.dependsOn applyResourceTask + + Task prepareTask = project.tasks.create("fastdexPrepareFor${variantName}", FastdexPrepareTask) + prepareTask.fastdexVariant = fastdexVariant + prepareTask.mustRunAfter variantOutput.processResources + + if (configuration.useCustomCompile) { + Task customJavacTask = project.tasks.create("fastdexCustomCompile${variantName}JavaWithJavac", FastdexCustomJavacTask) + customJavacTask.fastdexVariant = fastdexVariant + customJavacTask.dependsOn prepareTask + variant.javaCompile.dependsOn customJavacTask + } + else { + variant.javaCompile.dependsOn prepareTask + } + + Task multidexlistTask = getTransformClassesWithMultidexlistTask(project,variantName) + if (multidexlistTask != null) { + /** + * transformClassesWithMultidexlistFor${variantName}的作用是计算哪些类必须放在第一个dex里面,由于fastdex使用替换Application的方案隔离了项目代码的dex, + * 所以这个任务就没有存在的意义了,禁止掉这个任务以提高打包速度,但是transformClassesWithDexFor${variantName}会使用这个任务输出的txt文件, + * 所以就生成一个空文件防止报错 + */ + FastdexCreateMaindexlistFileTask createFileTask = project.tasks.create("fastdexCreate${variantName}MaindexlistFileTask", FastdexCreateMaindexlistFileTask) + createFileTask.fastdexVariant = fastdexVariant + + multidexlistTask.dependsOn createFileTask + multidexlistTask.enabled = false + } + + def collectMultiDexComponentsTask = getCollectMultiDexComponentsTask(project, variantName) + if (collectMultiDexComponentsTask != null) { + collectMultiDexComponentsTask.enabled = false + } + + FastdexInstantRunTask fastdexInstantRunTask = project.tasks.create("fastdex${variantName}",FastdexInstantRunTask) + fastdexInstantRunTask.fastdexVariant = fastdexVariant + fastdexInstantRunTask.resourceApFile = variantOutput.getVariantOutputData().getScope().getProcessResourcePackageOutputFile() + fastdexInstantRunTask.resDir = variantOutput.processResources.resDir + fastdexInstantRunTask.dependsOn variant.assemble + fastdexVariant.fastdexInstantRunTask = fastdexInstantRunTask + + getTransformClassesWithDex(project,variantName).doLast { + fastdexInstantRunTask.onDexTransformComplete() + } + + project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() { + @Override + public void graphPopulated(TaskExecutionGraph taskGraph) { + for (Task task : taskGraph.getAllTasks()) { + if (task.getProject().equals(project) + && task instanceof TransformTask + && task.name.toLowerCase().contains(variant.name.toLowerCase())) { + + Transform transform = ((TransformTask) task).getTransform() + //如果开启了multiDexEnabled true,存在transformClassesWithJarMergingFor${variantName}任务 + if ((((transform instanceof JarMergingTransform)) && !(transform instanceof FastdexJarMergingTransform))) { + fastdexVariant.hasJarMergingTask = true + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex find jarmerging transform. transform class: " + task.transform.getClass() + " . task name: " + task.name) + } + + FastdexJarMergingTransform jarMergingTransform = new FastdexJarMergingTransform(transform,fastdexVariant) + Field field = getFieldByName(task.getClass(),'transform') + field.setAccessible(true) + field.set(task,jarMergingTransform) + } + + if ((((transform instanceof DexTransform)) && !(transform instanceof FastdexTransform))) { + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name) + } + //代理DexTransform,实现自定义的转换 + FastdexTransform fastdexTransform = new FastdexTransform(transform,fastdexVariant) + Field field = getFieldByName(task.getClass(),'transform') + field.setAccessible(true) + field.set(task,fastdexTransform) + } + } + } + } + }); + + } + } + } + } + + Task getTinkerPatchManifestTask(Project project, String variantName) { + String tinkerPatchManifestTaskName = "tinkerpatchSupportProcess${variantName}Manifest" + try { + return project.tasks.getByName(tinkerPatchManifestTaskName) + } catch (Throwable e) { + return null + } + } + + Task getMergeDebugResources(Project project, String variantName) { + String mergeResourcesTaskName = "merge${variantName}Resources" + project.tasks.getByName(mergeResourcesTaskName) + } + + Task getTransformClassesWithMultidexlistTask(Project project, String variantName) { + String transformClassesWithMultidexlistTaskName = "transformClassesWithMultidexlistFor${variantName}" + try { + return project.tasks.getByName(transformClassesWithMultidexlistTaskName) + } catch (Throwable e) { + //fix issue #1 如果没有开启multidex会报错 + return null + } + } + + Task getTransformClassesWithDex(Project project, String variantName) { + String taskName = "transformClassesWithDexFor${variantName}" + return project.tasks.getByName(taskName) + } + + Task getCollectMultiDexComponentsTask(Project project, String variantName) { + try { + String collectMultiDexComponents = "collect${variantName}MultiDexComponents" + return project.tasks.findByName(collectMultiDexComponents) + } catch (Throwable e) { + return null + } + } + + Field getFieldByName(Class aClass, String name) { + Class currentClass = aClass; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(name); + } catch (NoSuchFieldException e) { + // ignored. + } + currentClass = currentClass.getSuperclass(); + } + return null; + } +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/extension/FastdexExtension.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/extension/FastdexExtension.groovy new file mode 100644 index 00000000..9af8a05a --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/extension/FastdexExtension.groovy @@ -0,0 +1,29 @@ +package com.dx168.fastdex.build.extension + +/** + * Created by tong on 17/10/3. + */ +public class FastdexExtension { + /** + * 是否可用 + */ + boolean fastdexEnable = true + /** + * debug模式下打印的日志稍微多一些 + */ + boolean debug = false + /** + * 是否换成fastdex的编译方式 + */ + boolean useCustomCompile = false + /** + * 每次都参与dex生成的class + */ + String[] hotClasses = [] + /** + * 当变化的java文件数量超过阈值,触发dex merge + */ + int dexMergeThreshold = 4 + + boolean handleReflectR = true +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCleanTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCleanTask.groovy new file mode 100644 index 00000000..774d74ec --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCleanTask.groovy @@ -0,0 +1,28 @@ +package com.dx168.fastdex.build.task + +import com.dx168.fastdex.build.util.FastdexUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * 清空指定variantName的缓存,如果variantName == null清空所有缓存 + * Created by tong on 17/3/12. + */ +public class FastdexCleanTask extends DefaultTask { + FastdexVariant fastdexVariant + + FastdexCleanTask() { + group = 'fastdex' + } + + @TaskAction + void clean() { + if (fastdexVariant == null) { + FastdexUtils.cleanAllCache(project) + } + else { + FastdexUtils.cleanCache(project,variantName) + } + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexConnectDeviceWithAdbTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexConnectDeviceWithAdbTask.groovy new file mode 100644 index 00000000..d06f37be --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexConnectDeviceWithAdbTask.groovy @@ -0,0 +1,58 @@ +package com.dx168.fastdex.build.task + +import com.android.ddmlib.AndroidDebugBridge +import com.android.ddmlib.IDevice +import com.dx168.fastdex.build.util.FastdexRuntimeException +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.TaskAction + +/** + * Created by tong on 17/4/27. + */ +public class FastdexConnectDeviceWithAdbTask extends DefaultTask { + FastdexInstantRunTask fastdexInstantRun + + FastdexConnectDeviceWithAdbTask() { + group = 'fastdex' + } + + @TaskAction + void connect() { + AndroidDebugBridge.initIfNeeded(false) + AndroidDebugBridge bridge = + AndroidDebugBridge.createBridge("/Users/tong/Applications/android-sdk-macosx/platform-tools/adb", false) + waitForDevice(bridge) + IDevice[] devices = bridge.getDevices() + IDevice device = null + + if (devices != null && devices.length > 0) { + device = devices[0] + } + + if (device == null) { + throw new FastdexRuntimeException("Device not found!!") + } + + if (devices.length > 1) { + throw new FastdexRuntimeException("Find multiple devices!!") + } + + fastdexInstantRun.device = device + project.logger.error("==fastdex device connected ${device.toString()}") + } + + private void waitForDevice(AndroidDebugBridge bridge) { + int count = 0; + while (!bridge.hasInitialDeviceList()) { + try { + Thread.sleep(100); + count++; + } catch (InterruptedException ignored) { + } + if (count > 300) { + throw new GradleException("Connect adb timeout!!") + } + } + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCreateMaindexlistFileTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCreateMaindexlistFileTask.groovy new file mode 100644 index 00000000..8348714a --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCreateMaindexlistFileTask.groovy @@ -0,0 +1,32 @@ +package com.dx168.fastdex.build.task + +import fastdex.common.utils.FileUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * transformClassesWithMultidexlistFor${variantName}的作用是计算哪些类必须放在第一个dex里面,由于fastdex使用替换Application的方案隔离了项目代码的dex, + * 所以这个任务就没有存在的意义了,禁止掉这个任务以提高打包速度,但是transformClassesWithDexFor${variantName}会使用这个任务输出的txt文件,所以需要生成一个空文件防止报错 + * Created by tong on 17/3/12. + */ +public class FastdexCreateMaindexlistFileTask extends DefaultTask { + FastdexVariant fastdexVariant + + FastdexCreateMaindexlistFileTask() { + group = 'fastdex' + } + + @TaskAction + void createFile() { + if (fastdexVariant.androidVariant != null) { + File maindexlistFile = fastdexVariant.androidVariant.getVariantData().getScope().getMainDexListFile() + File parentFile = maindexlistFile.getParentFile() + FileUtils.ensumeDir(parentFile) + + if (!FileUtils.isLegalFile(maindexlistFile)) { + maindexlistFile.createNewFile() + } + } + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCustomJavacTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCustomJavacTask.groovy new file mode 100755 index 00000000..5cf44fd9 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexCustomJavacTask.groovy @@ -0,0 +1,156 @@ +package com.dx168.fastdex.build.task + +import fastdex.build.lib.snapshoot.sourceset.PathInfo +import fastdex.build.lib.snapshoot.sourceset.SourceSetDiffResultSet +import com.dx168.fastdex.build.util.FastdexUtils +import fastdex.common.utils.FileUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.DefaultTask +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.TaskAction +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + * 每次SourceSet下的某个java文件变化时,默认的compile${variantName}JavaWithJavac任务会扫描所有的java文件 + * 处理javax.annotation.processing.AbstractProcessor接口用来代码动态代码生成,所以项目中的java文件如果很多会造成大量的时间浪费 + * + * 全量打包时使用默认的任务,补丁打包使用此任务以提高效率(仅编译变化的java文件不去扫描代码内容) + * + * https://ant.apache.org/manual/Tasks/javac.html + * + * Created by tong on 17/3/12. + */ +public class FastdexCustomJavacTask extends DefaultTask { + FastdexVariant fastdexVariant + + FastdexCustomJavacTask() { + group = 'fastdex' + } + + @TaskAction + void compile() { + def compileTask = fastdexVariant.androidVariant.javaCompile + compileTask.enabled = true + + def project = fastdexVariant.project + def projectSnapshoot = fastdexVariant.projectSnapshoot + + File classesDir = fastdexVariant.androidVariant.getVariantData().getScope().getJavaOutputDir() + if (!FileUtils.dirExists(classesDir.absolutePath)) { + return + } + + if (!fastdexVariant.configuration.useCustomCompile) { + return + } + + if (!fastdexVariant.hasDexCache) { + return + } + + SourceSetDiffResultSet sourceSetDiffResultSet = projectSnapshoot.diffResultSet + //java文件是否发生变化 + if (!sourceSetDiffResultSet.isJavaFileChanged()) { + project.logger.error("==fastdex no java files changed, just ignore") + compileTask.enabled = false + return + } + + //此次变化是否和上次的变化一样 + if (projectSnapshoot.diffResultSet != null + && projectSnapshoot.oldDiffResultSet != null + && projectSnapshoot.diffResultSet.equals(projectSnapshoot.oldDiffResultSet)) { + project.logger.error("==fastdex java files not changed, just ignore") + compileTask.enabled = false + return + } + Set addOrModifiedPathInfos = sourceSetDiffResultSet.addOrModifiedPathInfos + + File patchJavaFileDir = new File(FastdexUtils.getWorkDir(project,fastdexVariant.variantName),"custom-combind") + File patchClassesFileDir = new File(FastdexUtils.getWorkDir(project,fastdexVariant.variantName),"custom-combind-classes") + FileUtils.deleteDir(patchJavaFileDir) + FileUtils.ensumeDir(patchClassesFileDir) + + for (PathInfo pathInfo : addOrModifiedPathInfos) { + project.logger.error("==fastdex changed java file: ${pathInfo.relativePath}") + FileUtils.copyFileUsingStream(pathInfo.absoluteFile,new File(patchJavaFileDir,pathInfo.relativePath)) + } + + //处理动态生成的java文件 + handleApt(addOrModifiedPathInfos,patchJavaFileDir) + + //compile java + File androidJar = new File("${FastdexUtils.getSdkDirectory(project)}${File.separator}platforms${File.separator}${project.android.getCompileSdkVersion()}${File.separator}android.jar") + File classpathJar = FastdexUtils.getInjectedJarFile(project,fastdexVariant.variantName) + + //def classpath = project.files(classpathJar.absolutePath) + compileTask.classpath + + def classpath = project.files(classpathJar.absolutePath) + def fork = compileTask.options.fork + def executable = compileTask.options.forkOptions.executable + + project.logger.error("==fastdex executable ${executable}") + //处理retrolambda + if (project.plugins.hasPlugin("me.tatarka.retrolambda")) { + fork = true + //def retrolambda = project.extensions.getByType(RetrolambdaExtension) + def retrolambda = project.retrolambda + def rt = "$retrolambda.jdk/jre/lib/rt.jar" + classpath = classpath + project.files(rt) + executable = "${retrolambda.tryGetJdk()}/bin/javac" + } + project.logger.error("==fastdex androidJar: ${androidJar}") + project.logger.error("==fastdex classpath: ${classpath.files}") + + //https://ant.apache.org/manual/Tasks/javac.html + //最好检测下项目根目录的gradle.properties文件,是否有这个配置org.gradle.jvmargs=-Dfile.encoding=UTF-8 + project.ant.javac( + srcdir: patchJavaFileDir, + destdir: patchClassesFileDir, + source: compileTask.sourceCompatibility, + target: compileTask.targetCompatibility, + encoding: 'UTF-8', + bootclasspath: androidJar, + classpath: joinClasspath(classpath), + fork: fork, + executable: executable + ) + + project.logger.error("==fastdex compile success: ${patchClassesFileDir}") + + //覆盖app/build/intermediates/classes内容 + Files.walkFileTree(patchClassesFileDir.toPath(),new SimpleFileVisitor(){ + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path relativePath = patchClassesFileDir.toPath().relativize(file) + File destFile = new File(classesDir,relativePath.toString()) + FileUtils.copyFileUsingStream(file.toFile(),destFile) + + project.logger.error("==fastdex apply class to ${destFile}") + return FileVisitResult.CONTINUE + } + }) + compileTask.enabled = false + //保存对比信息 + fastdexVariant.projectSnapshoot.saveDiffResultSet() + } + + def handleApt(Set addOrModifiedPathInfos, File patchJavaFileDir) { + //TODO 扫描apt目录 + } + + def joinClasspath(FileCollection collection) { + StringBuilder sb = new StringBuilder() + collection.files.each { file -> + sb.append(file.absolutePath) + sb.append(":") + } + if (sb.toString().endsWith(":")) { + sb.deleteCharAt(sb.length() - 1) + } + return sb + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexInstantRunTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexInstantRunTask.groovy new file mode 100644 index 00000000..0cef698a --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexInstantRunTask.groovy @@ -0,0 +1,303 @@ +package com.dx168.fastdex.build.task + +import com.android.ddmlib.AndroidDebugBridge +import com.android.ddmlib.IDevice +import com.dx168.fastdex.build.util.FastdexRuntimeException +import com.dx168.fastdex.build.util.FastdexUtils +import com.dx168.fastdex.build.util.GradleUtils +import com.dx168.fastdex.build.util.MetaInfo +import com.dx168.fastdex.build.variant.FastdexVariant +import fastdex.build.lib.fd.Communicator +import fastdex.build.lib.fd.ServiceCommunicator +import fastdex.common.ShareConstants +import fastdex.common.fd.ProtocolConstants +import fastdex.common.utils.FileUtils +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * Created by tong on 17/3/12. + */ +public class FastdexInstantRunTask extends DefaultTask { + FastdexVariant fastdexVariant + File resourceApFile + String resDir + boolean alreadySendPatch + IDevice device + + FastdexInstantRunTask() { + group = 'fastdex' + } + + private void waitForDevice(AndroidDebugBridge bridge) { + int count = 0; + while (!bridge.hasInitialDeviceList()) { + try { + Thread.sleep(100); + count++; + } catch (InterruptedException ignored) { + } + if (count > 300) { + throw new FastdexRuntimeException("Connect adb timeout!!") + } + } + } + + def preparedDevice() { + if (device != null) { + return + } + AndroidDebugBridge.initIfNeeded(false) + AndroidDebugBridge bridge = + AndroidDebugBridge.createBridge(FastdexUtils.getAdbCmdPath(project), false) + waitForDevice(bridge) + IDevice[] devices = bridge.getDevices() + if (devices != null && devices.length > 0) { + if (devices.length > 1) { + throw new FastdexRuntimeException("Find multiple devices!!") + } + device = devices[0] + } + + if (device == null) { + throw new FastdexRuntimeException("Device not found!!") + } + project.logger.error("==fastdex device connected ${device.toString()}") + } + + public void onDexTransformComplete() { + if (!isInstantRunBuild()) { + return + } + preparedDevice() + def packageName = fastdexVariant.getMergedPackageName() + ServiceCommunicator serviceCommunicator = new ServiceCommunicator(packageName) + try { + boolean active = false + int appPid = -1 + MetaInfo runtimeMetaInfo = serviceCommunicator.talkToService(device, new Communicator() { + @Override + public MetaInfo communicate(DataInputStream input, DataOutputStream output) throws IOException { + output.writeInt(ProtocolConstants.MESSAGE_PING) + MetaInfo runtimeMetaInfo = new MetaInfo() + active = input.readBoolean() + runtimeMetaInfo.buildMillis = input.readLong() + runtimeMetaInfo.variantName = input.readUTF() + appPid = input.readInt() + return runtimeMetaInfo + } + }) + project.logger.error("==fastdex receive: ${runtimeMetaInfo}") + if (fastdexVariant.metaInfo.buildMillis != runtimeMetaInfo.buildMillis) { + throw new IOException("buildMillis not equal") + } + if (!fastdexVariant.metaInfo.variantName.equals(runtimeMetaInfo.variantName)) { + throw new IOException("variantName not equal") + } + + File resourcesApk = FastdexUtils.getResourcesApk(project,fastdexVariant.variantName) + generateResourceApk(resourcesApk) + File mergedPatchDex = FastdexUtils.getMergedPatchDex(fastdexVariant.project,fastdexVariant.variantName) + File patchDex = FastdexUtils.getPatchDexFile(fastdexVariant.project,fastdexVariant.variantName) + + int changeCount = 1 + if (FileUtils.isLegalFile(mergedPatchDex)) { + changeCount += 1 + } + if (FileUtils.isLegalFile(patchDex)) { + changeCount += 1 + } + + long start = System.currentTimeMillis() + + serviceCommunicator.talkToService(device, new Communicator() { + @Override + public Boolean communicate(DataInputStream input, DataOutputStream output) throws IOException { + output.writeInt(ProtocolConstants.MESSAGE_PATCHES) + output.writeLong(0L) + output.writeInt(changeCount) + + project.logger.error("==fastdex write ${ShareConstants.RESOURCE_APK_FILE_NAME}") + output.writeUTF(ShareConstants.RESOURCE_APK_FILE_NAME) + byte[] bytes = FileUtils.readContents(resourcesApk) + output.writeInt(bytes.length) + output.write(bytes) + if (FileUtils.isLegalFile(mergedPatchDex)) { + project.logger.error("==fastdex write ${mergedPatchDex}") + output.writeUTF(ShareConstants.MERGED_PATCH_DEX) + bytes = FileUtils.readContents(mergedPatchDex) + output.writeInt(bytes.length) + output.write(bytes) + } + if (FileUtils.isLegalFile(patchDex)) { + project.logger.error("==fastdex write ${patchDex}") + output.writeUTF(ShareConstants.PATCH_DEX) + bytes = FileUtils.readContents(patchDex) + output.writeInt(bytes.length) + output.write(bytes) + } + + output.writeInt(ProtocolConstants.UPDATE_MODE_WARM_SWAP) + output.writeBoolean(true) + + return input.readBoolean() + } + }) + long end = System.currentTimeMillis(); + project.logger.error("==fastdex send patch data success. use: ${end - start}ms") + + //kill app + killApp(appPid) + startBootActivity() + + //project.tasks.getByName("validateSigning${fastdexVariant.variantName}").enabled = false + project.tasks.getByName("package${fastdexVariant.variantName}").enabled = false + project.tasks.getByName("assemble${fastdexVariant.variantName}").enabled = false + alreadySendPatch = true + } catch (IOException e) { + if (fastdexVariant.configuration.debug) { + e.printStackTrace() + } + } + } + + @TaskAction + void instantRun() { + if (alreadySendPatch) { + return + } + + preparedDevice() + normalRun(device) + } + + void normalRun(IDevice device) { + def targetVariant = fastdexVariant.androidVariant + project.logger.error("==fastdex normal run ${fastdexVariant.variantName}") + //安装app + File apkFile = targetVariant.outputs.first().getOutputFile() + project.logger.error("adb install -r ${apkFile}") + device.installPackage(apkFile.absolutePath,true) + startBootActivity() + } + + def killApp(appPid) { + if (appPid == -1) { + return + } + //$ adb shell kill {appPid} + def process = new ProcessBuilder(FastdexUtils.getAdbCmdPath(project),"shell","kill","${appPid}").start() + int status = process.waitFor() + try { + process.destroy() + } catch (Throwable e) { + + } + + String cmd = "adb shell kill ${appPid}" + if (fastdexVariant.configuration.debug) { + project.logger.error("${cmd}") + } + if (status != 0) { + throw new RuntimeException("==fastdex kill app fail: \n${cmd}") + } + } + + def startBootActivity() { + def packageName = fastdexVariant.getMergedPackageName() + + //启动第一个activity + String bootActivityName = GradleUtils.getBootActivity(fastdexVariant.manifestPath) + if (bootActivityName) { + //$ adb shell am start -n "com.dx168.fastdex.sample/com.dx168.fastdex.sample.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER + def process = new ProcessBuilder(FastdexUtils.getAdbCmdPath(project),"shell","am","start","-n","\"${packageName}/${bootActivityName}\"","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER").start() + int status = process.waitFor() + try { + process.destroy() + } catch (Throwable e) { + + } + + String cmd = "adb shell am start -n \"${packageName}/${bootActivityName}\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER" + if (fastdexVariant.configuration.debug) { + project.logger.error("${cmd}") + } + if (status != 0) { + throw new RuntimeException("==fastdex start activity fail: \n${cmd}") + } + } + } + + def generateResourceApk(File resourcesApk) { + long start = System.currentTimeMillis() + File tempDir = new File(FastdexUtils.getResourceDir(project,fastdexVariant.variantName),"temp") + FileUtils.cleanDir(tempDir) + File resourceAp = new File(project.buildDir,"intermediates${File.separator}res${File.separator}resources-debug.ap_") + + File tempResourcesApk = new File(tempDir,resourcesApk.getName()) + FileUtils.copyFileUsingStream(resourceAp,tempResourcesApk) + + File assetsPath = fastdexVariant.androidVariant.getVariantData().getScope().getMergeAssetsOutputDir() + List assetFiles = getAssetFiles(assetsPath) + if (assetFiles.isEmpty()) { + return + } + File tempAssetsPath = new File(tempDir,"assets") + FileUtils.copyDir(assetsPath,tempAssetsPath) + + String[] cmds = new String[assetFiles.size() + 4] + cmds[0] = FastdexUtils.getAaptCmdPath(project) + cmds[1] = "add" + cmds[2] = "-f" + cmds[3] = tempResourcesApk.absolutePath + for (int i = 0; i < assetFiles.size(); i++) { + cmds[4 + i] = "assets/${assetFiles.get(i)}"; + } + + ProcessBuilder aaptProcess = new ProcessBuilder(cmds) + aaptProcess.directory(tempDir) + def process = aaptProcess.start() + int status = process.waitFor() + try { + process.destroy() + } catch (Throwable e) { + + } + + tempResourcesApk.renameTo(resourcesApk) + def cmd = cmds.join(" ") + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex add asset files into resources.apk. cmd:\n${cmd}") + } + if (status != 0) { + throw new RuntimeException("==fastdex add asset files into resources.apk fail. cmd:\n${cmd}") + } + long end = System.currentTimeMillis(); + fastdexVariant.project.logger.error("==fastdex generate resources.apk success: \n==${resourcesApk} use: ${end - start}ms") + } + + List getAssetFiles(File dir) { + ArrayList result = new ArrayList<>() + if (dir == null || !FileUtils.dirExists(dir.getAbsolutePath())) { + return result + } + if (dir.listFiles().length == 0) { + return result + } + for (File file : dir.listFiles()) { + if (file.isFile() && !file.getName().startsWith(".")) { + result.add(file.getName()) + } + } + return result; + } + + def isInstantRunBuild() { + String launchTaskName = project.gradle.startParameter.taskRequests.get(0).args.get(0).toString() + boolean result = launchTaskName.endsWith("fastdex${fastdexVariant.variantName}") + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex launchTaskName: ${launchTaskName}") + } + return result + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexManifestTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexManifestTask.groovy new file mode 100644 index 00000000..34484820 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexManifestTask.groovy @@ -0,0 +1,66 @@ +package com.dx168.fastdex.build.task + +import com.dx168.fastdex.build.variant.FastdexVariant +import groovy.xml.Namespace +import groovy.xml.QName +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * 替换项目的Application为fastdex.runtime.FastdexApplication + * 并且在Manifest文件里中添加下面的节点 + * + * + * Created by tong on 17/3/11. + */ +public class FastdexManifestTask extends DefaultTask { + static final String MANIFEST_XML = "AndroidManifest.xml" + static final String FASTDEX_ORIGIN_APPLICATION_CLASSNAME = "FASTDEX_ORIGIN_APPLICATION_CLASSNAME" + + FastdexVariant fastdexVariant + + FastdexManifestTask() { + group = 'fastdex' + } + + @TaskAction + def updateManifest() { + def ns = new Namespace("http://schemas.android.com/apk/res/android", "android") + + def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(fastdexVariant.manifestPath), "utf-8")) + + def application = xml.application[0] + if (application) { + QName nameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android'); + def applicationName = application.attribute(nameAttr) + if (applicationName == null || applicationName.isEmpty()) { + applicationName = "android.app.Application" + } + application.attributes().put(nameAttr, "fastdex.runtime.FastdexApplication") + + def metaDataTags = application['meta-data'] + + // remove any old FASTDEX_ORIGIN_APPLICATION_CLASSNAME elements + def originApplicationName = metaDataTags.findAll { + it.attributes()[ns.name].equals(FASTDEX_ORIGIN_APPLICATION_CLASSNAME) + }.each { + it.parent().remove(it) + } + + // Add the new FASTDEX_ORIGIN_APPLICATION_CLASSNAME element + application.appendNode('meta-data', [(ns.name): FASTDEX_ORIGIN_APPLICATION_CLASSNAME, (ns.value): applicationName]) + + // Write the manifest file + def printer = new XmlNodePrinter(new PrintWriter(fastdexVariant.manifestPath, "utf-8")) + printer.preserveWhitespace = true + printer.print(xml) + } + File manifestFile = new File(fastdexVariant.manifestPath) +// if (manifestFile.exists()) { +// File buildDir = FastdexUtils.getBuildDir(project,fastdexVariant.variantName) +// FileUtils.copyFileUsingStream(manifestFile, new File(buildDir,MANIFEST_XML)) +// project.logger.error("fastdex gen AndroidManifest.xml in ${MANIFEST_XML}") +// } + } +} + diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexPrepareTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexPrepareTask.groovy new file mode 100644 index 00000000..4bf2405e --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexPrepareTask.groovy @@ -0,0 +1,22 @@ +package com.dx168.fastdex.build.task + +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * 准备上下文环境 + * Created by tong on 17/4/18. + */ +public class FastdexPrepareTask extends DefaultTask { + FastdexVariant fastdexVariant + + FastdexPrepareTask() { + group = 'fastdex' + } + + @TaskAction + void prepareContext() { + fastdexVariant.prepareEnv() + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexResourceIdTask.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexResourceIdTask.groovy new file mode 100644 index 00000000..56b795c9 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/task/FastdexResourceIdTask.groovy @@ -0,0 +1,88 @@ +package com.dx168.fastdex.build.task + +import com.dx168.fastdex.build.util.Constants +import fastdex.common.utils.FileUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import fastdex.build.lib.aapt.AaptResourceCollector +import fastdex.build.lib.aapt.AaptUtil +import fastdex.build.lib.aapt.PatchUtil +import fastdex.build.lib.aapt.RDotTxtEntry +import com.dx168.fastdex.build.util.FastdexUtils + +/** + * 保持补丁打包时R文件中相同的节点和第一次打包时的值保持一致 + * + * 把第一次打包时生成的build/intermediates/symbols/${variant}/R.txt保存下来, + * 补丁打包时使用R.txt作为输入生成public.xml和ids.xml并放进build/intermediates/res/merged/${variant}/values里面 + * + * 详情请看老罗的文章和tinker项目的实现 + * http://blog.csdn.net/luoshengyang/article/details/8744683 + * https://github.com/Tencent/tinker/tree/master/tinker-build/tinker-patch-gradle-plugin + * + * Created by tong on 17/3/11. + */ +public class FastdexResourceIdTask extends DefaultTask { + FastdexVariant fastdexVariant + String resDir + + FastdexResourceIdTask() { + group = 'fastdex' + } + + @TaskAction + def applyResourceId() { + String resourceMappingFile = FastdexUtils.getResourceMappingFile(project,fastdexVariant.variantName) + + // Parse the public.xml and ids.xml + if (!FileUtils.isLegalFile(new File(resourceMappingFile))) { + project.logger.error("==fastdex apply resource mapping file ${resourceMappingFile} is illegal, just ignore") + return + } + + File idsXmlFile = FastdexUtils.getIdxXmlFile(project,fastdexVariant.variantName) + File publicXmlFile = FastdexUtils.getPublicXmlFile(project,fastdexVariant.variantName) + + String idsXml = resDir + "/values/ids.xml"; + String publicXml = resDir + "/values/public.xml"; + File resDirIdsXmlFile = new File(idsXml) + File resDirPublicXmlFile = new File(publicXml) + + if (FileUtils.isLegalFile(idsXmlFile) && FileUtils.isLegalFile(publicXmlFile)) { + if (!FileUtils.isLegalFile(resDirIdsXmlFile) || idsXmlFile.lastModified() != resDirIdsXmlFile.lastModified()) { + FileUtils.copyFileUsingStream(idsXmlFile,resDirIdsXmlFile) + project.logger.error("==fastdex apply cached resource idx.xml ${idsXml}") + } + + if (!FileUtils.isLegalFile(resDirPublicXmlFile) || publicXmlFile.lastModified() != resDirPublicXmlFile.lastModified()) { + FileUtils.copyFileUsingStream(publicXmlFile,resDirPublicXmlFile) + project.logger.error("==fastdex apply cached resource public.xml ${publicXml}") + } + return + } + + FileUtils.deleteFile(idsXml); + FileUtils.deleteFile(publicXml); + List resourceDirectoryList = new ArrayList() + resourceDirectoryList.add(resDir) + + project.logger.error("==fastdex we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}") + Map> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile) + + AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap) + PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml) + File publicFile = new File(publicXml) + + if (publicFile.exists()) { + FileUtils.copyFileUsingStream(publicFile, publicXmlFile) + project.logger.error("==fastdex gen resource public.xml in ${Constants.RESOURCE_PUBLIC_XML}") + } + File idxFile = new File(idsXml) + if (idxFile.exists()) { + FileUtils.copyFileUsingStream(idxFile, idsXmlFile) + project.logger.error("==fastdex gen resource idx.xml in ${Constants.RESOURCE_IDX_XML}") + } + } +} + diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexJarMergingTransform.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexJarMergingTransform.groovy new file mode 100644 index 00000000..988c9947 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexJarMergingTransform.groovy @@ -0,0 +1,60 @@ +package com.dx168.fastdex.build.transform + +import com.android.build.api.transform.Transform +import com.android.build.api.transform.TransformException +import com.android.build.api.transform.TransformInvocation +import com.dx168.fastdex.build.util.ClassInject +import com.dx168.fastdex.build.util.JarOperation +import com.dx168.fastdex.build.variant.FastdexVariant +import com.android.build.api.transform.Format +import fastdex.common.utils.FileUtils + +/** + * 拦截transformClassesWithJarMergingFor${variantName}任务, + * Created by tong on 17/27/3. + */ +class FastdexJarMergingTransform extends TransformProxy { + FastdexVariant fastdexVariant + + FastdexJarMergingTransform(Transform base, FastdexVariant fastdexVariant) { + super(base) + this.fastdexVariant = fastdexVariant + } + + @Override + void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException { + if (fastdexVariant.hasDexCache) { + if (fastdexVariant.projectSnapshoot.diffResultSet.isJavaFileChanged()) { + //补丁jar + File patchJar = getCombinedJarFile(transformInvocation) + //生成补丁jar + JarOperation.generatePatchJar(fastdexVariant,transformInvocation,patchJar) + } + else { + fastdexVariant.project.logger.error("==fastdex no java files have changed, just ignore") + } + } + else { + //inject dir input + ClassInject.injectTransformInvocation(fastdexVariant,transformInvocation) + base.transform(transformInvocation) + } + + fastdexVariant.executedJarMerge = true + } + + /** + * 获取输出jar路径 + * @param invocation + * @return + */ + public File getCombinedJarFile(TransformInvocation invocation) { + def outputProvider = invocation.getOutputProvider(); + + // all the output will be the same since the transform type is COMBINED. + // and format is SINGLE_JAR so output is a jar + File jarFile = outputProvider.getContentLocation("combined", base.getOutputTypes(), base.getScopes(), Format.JAR); + FileUtils.ensumeDir(jarFile.getParentFile()); + return jarFile + } +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexTransform.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexTransform.groovy new file mode 100644 index 00000000..45c057ea --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/FastdexTransform.groovy @@ -0,0 +1,382 @@ +package com.dx168.fastdex.build.transform + +import com.android.build.api.transform.Transform +import com.android.build.api.transform.TransformException +import com.android.build.api.transform.TransformInvocation +import com.dx168.fastdex.build.util.ClassInject +import com.dx168.fastdex.build.util.Constants +import com.dx168.fastdex.build.util.DexOperation +import com.dx168.fastdex.build.util.FastdexUtils +import com.dx168.fastdex.build.util.GradleUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import com.google.common.collect.Lists +import org.gradle.api.Project +import fastdex.common.utils.FileUtils +import com.android.build.api.transform.JarInput +import com.android.build.api.transform.TransformInput +import com.dx168.fastdex.build.util.JarOperation + +/** + * 用于dex生成 + * 全量打包时的流程: + * 1、合并所有的class文件生成一个jar包 + * 2、扫描所有的项目代码并且在构造方法里添加对com.dx168.fastdex.runtime.antilazyload.AntilazyLoad类的依赖 + * 这样做的目的是为了解决class verify的问题, + * 详情请看https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a + * 3、对项目代码做快照,为了以后补丁打包时对比那些java文件发生了变化 + * 4、对当前项目的所以依赖做快照,为了以后补丁打包时对比依赖是否发生了变化,如果变化需要清除缓存 + * 5、调用真正的transform生成dex + * 6、缓存生成的dex,并且把fastdex-runtime.dex插入到dex列表中,假如生成了两个dex,classes.dex classes2.dex 需要做一下操作 + * fastdex-runtime.dex => classes.dex + * classes.dex => classes2.dex + * classes2.dex => classes3.dex + * 然后运行期在入口Application(com.dx168.fastdex.runtime.FastdexApplication)使用MultiDex把所有的dex加载进来 + * 7、保存资源映射映射表,为了保持id的值一致,详情看 + * @see com.dx168.fastdex.build.task.FastdexResourceIdTask + * + * 补丁打包时的流程 + * 1、检查缓存的有效性 + * @see com.dx168.fastdex.build.task.FastdexCustomJavacTask 的prepareEnv方法说明 + * 2、扫描所有变化的java文件并编译成class + * @see com.dx168.fastdex.build.task.FastdexCustomJavacTask + * 3、合并所有变化的class并生成jar包 + * 4、生成补丁dex + * 5、把所有的dex按照一定规律放在transformClassesWithMultidexlistFor${variantName}任务的输出目录 + * fastdex-runtime.dex => classes.dex + * patch.dex => classes2.dex + * dex_cache.classes.dex => classes3.dex + * dex_cache.classes2.dex => classes4.dex + * dex_cache.classesN.dex => classes(N + 2).dex + * + * Created by tong on 17/10/3. + */ +class FastdexTransform extends TransformProxy { + FastdexVariant fastdexVariant + + Project project + String variantName + + FastdexTransform(Transform base, FastdexVariant fastdexVariant) { + super(base) + this.fastdexVariant = fastdexVariant + this.project = fastdexVariant.project + this.variantName = fastdexVariant.variantName + } + + @Override + void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException { + if (fastdexVariant.hasDexCache) { + project.logger.error("==fastdex patch transform start,we will generate dex file") + if (fastdexVariant.projectSnapshoot.diffResultSet.isJavaFileChanged()) { + //生成补丁jar包 + File patchJar = generatePatchJar(transformInvocation) + File patchDex = FastdexUtils.getPatchDexFile(fastdexVariant.project,fastdexVariant.variantName) + + DexOperation.generatePatchDex(fastdexVariant,base,patchJar,patchDex) + //获取dex输出路径 + File dexOutputDir = GradleUtils.getDexOutputDir(project,base,transformInvocation) + //merged dex + File mergedPatchDexDir = FastdexUtils.getMergedPatchDexDir(fastdexVariant.project,fastdexVariant.variantName) + + if (fastdexVariant.willExecDexMerge()) { + //merge dex + if (FileUtils.hasDex(mergedPatchDexDir)) { + //已经执行过一次dex merge + File cacheDexDir = FastdexUtils.getDexCacheDir(project,variantName) + //File outputDex = new File(dexOutputDir,"merged-patch.dex") + File mergedPatchDex = new File(mergedPatchDexDir,Constants.CLASSES_DEX) + //更新patch.dex + DexOperation.mergeDex(fastdexVariant,mergedPatchDex,patchDex,mergedPatchDex) + FileUtils.cleanDir(dexOutputDir) + + FileUtils.copyDir(cacheDexDir,dexOutputDir,Constants.DEX_SUFFIX) + + incrementDexDir(dexOutputDir,2) + //copy merged-patch.dex + FileUtils.copyFileUsingStream(mergedPatchDex,new File(dexOutputDir,"${Constants.CLASSES}2${Constants.DEX_SUFFIX}")) + //copy fastdex-runtime.dex + FileUtils.copyResourceUsingStream(Constants.RUNTIME_DEX_FILENAME,new File(dexOutputDir,Constants.CLASSES_DEX)) + } + else { + //第一只执行dex merge,直接保存patchDex + //patch.dex => classes.dex + //dex_cache.classes.dex => classes2.dex + //dex_cache.classes2.dex => classes3.dex + //dex_cache.classesN.dex => classes(N + 1).dex + //复制补丁dex到输出路径 + hookPatchBuildDex(dexOutputDir,mergedPatchDexDir,patchDex) + + FileUtils.cleanDir(mergedPatchDexDir) + FileUtils.ensumeDir(mergedPatchDexDir) + patchDex.renameTo(new File(mergedPatchDexDir,Constants.CLASSES_DEX)) + } + fastdexVariant.onDexGenerateSuccess(false,true) + } + else { + fastdexVariant.metaInfo.patchDexVersion += 1 + //复制补丁打包的dex到输出路径 + hookPatchBuildDex(dexOutputDir,mergedPatchDexDir,patchDex) + fastdexVariant.onDexGenerateSuccess(false,false) + } + } + else { + project.logger.error("==fastdex no java files have changed, just ignore") + } + } + else { + def config = fastdexVariant.androidVariant.getVariantData().getVariantConfiguration() + boolean isMultiDexEnabled = config.isMultiDexEnabled() + + project.logger.error("==fastdex normal transform start") + if (isMultiDexEnabled) { + if (fastdexVariant.executedJarMerge) { + //如果开启了multidex,FastdexJarMergingTransform完成了inject的操作,不需要在做处理 + File combinedJar = getCombinedJarFile(transformInvocation) + + if (fastdexVariant.configuration.useCustomCompile) { + File injectedJar = FastdexUtils.getInjectedJarFile(project,variantName) + FileUtils.copyFileUsingStream(combinedJar,injectedJar) + } + } else { + ClassInject.injectTransformInvocation(fastdexVariant,transformInvocation) + File injectedJar = FastdexUtils.getInjectedJarFile(project,variantName) + GradleUtils.executeMerge(project,transformInvocation,injectedJar) + transformInvocation = GradleUtils.createNewTransformInvocation(base,transformInvocation,injectedJar) + } + } + else { + //如果没有开启multidex需要在此处做注入 + ClassInject.injectTransformInvocation(fastdexVariant,transformInvocation) + if (fastdexVariant.configuration.useCustomCompile) { + File injectedJar = FastdexUtils.getInjectedJarFile(project,variantName) + GradleUtils.executeMerge(project,transformInvocation,injectedJar) + } + } + //调用默认转换方法 + base.transform(transformInvocation) + //获取dex输出路径 + File dexOutputDir = GradleUtils.getDexOutputDir(project,base,transformInvocation) + //缓存dex + int dexCount = cacheNormalBuildDex(dexOutputDir) + //复制全量打包的dex到输出路径 + hookNormalBuildDex(dexOutputDir) + + fastdexVariant.metaInfo.dexCount = dexCount + fastdexVariant.metaInfo.buildMillis = System.currentTimeMillis() + + fastdexVariant.onDexGenerateSuccess(true,false) + project.logger.error("==fastdex normal transform end") + } + + fastdexVariant.executedDexTransform = true + } + + /** + * 获取输出jar路径 + * @param invocation + * @return + */ + public File getCombinedJarFile(TransformInvocation invocation) { + List jarInputs = Lists.newArrayList(); + for (TransformInput input : invocation.getInputs()) { + jarInputs.addAll(input.getJarInputs()); + } + if (jarInputs.size() != 1) { + throw new RuntimeException("==fastdex jar input size is ${jarInputs.size()}, expected is 1") + } + File combinedJar = jarInputs.get(0).getFile() + return combinedJar + } + + /** + * 生成补丁jar包 + * @param transformInvocation + * @return + */ + File generatePatchJar(TransformInvocation transformInvocation) { + def config = fastdexVariant.androidVariant.getVariantData().getVariantConfiguration() + boolean isMultiDexEnabled = config.isMultiDexEnabled() + if (isMultiDexEnabled && (fastdexVariant.executedJarMerge || fastdexVariant.hasJarMergingTask)) { + //如果开启了multidex,FastdexJarMergingTransform完成了jar merge的操作 + File patchJar = getCombinedJarFile(transformInvocation) + project.logger.error("==fastdex multiDex enabled use patch.jar: ${patchJar}") + return patchJar + } + else { + //补丁jar + File patchJar = new File(FastdexUtils.getBuildDir(project,variantName),"patch-combined.jar") + //生成补丁jar + JarOperation.generatePatchJar(fastdexVariant,transformInvocation,patchJar) + return patchJar + } + } + + /** + * 缓存全量打包时生成的dex + * @param dexOutputDir dex输出路径 + */ + int cacheNormalBuildDex(File dexOutputDir) { + project.logger.error("==fastdex dex output directory: " + dexOutputDir) + + int dexCount = 0 + File cacheDexDir = FastdexUtils.getDexCacheDir(project,variantName) + File[] files = dexOutputDir.listFiles() + files.each { file -> + if (file.getName().endsWith(Constants.DEX_SUFFIX)) { + FileUtils.copyFileUsingStream(file,new File(cacheDexDir,file.getName())) + dexCount = dexCount + 1 + } + } + return dexCount + } + + void incrementDexDir(File dexDir) { + incrementDexDir(dexDir,1) + } + + /** + * 递增指定目录中的dex + * + * classes.dex => classes2.dex + * classes2.dex => classes3.dex + * classesN.dex => classes(N + 1).dex + * + * @param dexDir + */ + void incrementDexDir(File dexDir,int dsize) { + if (dsize <= 0) { + throw new RuntimeException("dsize must be greater than 0!") + } + //classes.dex => classes2.dex.tmp + //classes2.dex => classes3.dex.tmp + //classesN.dex => classes(N + 1).dex.tmp + + String tmpSuffix = ".tmp" + File classesDex = new File(dexDir,Constants.CLASSES_DEX) + if (FileUtils.isLegalFile(classesDex)) { + classesDex.renameTo(new File(dexDir,"classes${dsize + 1}.dex${tmpSuffix}")) + } + int point = 2 + File dexFile = new File(dexDir,"${Constants.CLASSES}${point}${Constants.DEX_SUFFIX}") + while (FileUtils.isLegalFile(dexFile)) { + new File(dexDir,"classes${point}.dex").renameTo(new File(dexDir,"classes${point + dsize}.dex${tmpSuffix}")) + point++ + dexFile = new File(dexDir,"classes${point}.dex") + } + + //classes2.dex.tmp => classes2.dex + //classes3.dex.tmp => classes3.dex + //classesN.dex.tmp => classesN.dex + point = dsize + 1 + dexFile = new File(dexDir,"classes${point}.dex${tmpSuffix}") + while (FileUtils.isLegalFile(dexFile)) { + dexFile.renameTo(new File(dexDir,"classes${point}.dex")) + point++ + dexFile = new File(dexDir,"classes${point}.dex${tmpSuffix}") + } + } + + /** + * 全量打包时复制dex到指定位置 + * @param dexOutputDir dex输出路径 + */ + void hookNormalBuildDex(File dexOutputDir) { + //dexelements [fastdex-runtime.dex ${dex_cache}.listFiles] + //runtime.dex => classes.dex + //dex_cache.classes.dex => classes2.dex + //dex_cache.classes2.dex => classes3.dex + //dex_cache.classesN.dex => classes(N + 1).dex + + incrementDexDir(dexOutputDir) + + //fastdex-runtime.dex = > classes.dex + FileUtils.copyResourceUsingStream(Constants.RUNTIME_DEX_FILENAME,new File(dexOutputDir,Constants.CLASSES_DEX)) + printLogWhenDexGenerateComplete(dexOutputDir,true) + } + + /** + * 补丁打包时复制dex到指定位置 + * @param dexOutputDir dex输出路径 + */ + void hookPatchBuildDex(File dexOutputDir,File mergedPatchDexDir,File patchDex) { + //dexelements [fastdex-runtime.dex patch.dex ${dex_cache}.listFiles] + //runtime.dex => classes.dex + //patch.dex => classes2.dex + //dex_cache.classes.dex => classes3.dex + //dex_cache.classes2.dex => classes4.dex + //dex_cache.classesN.dex => classes(N + 2).dex + project.logger.error("==fastdex patch transform hook patch dex start") + + FileUtils.cleanDir(dexOutputDir) + File mergedPatchDex = new File(mergedPatchDexDir,Constants.CLASSES_DEX) + File cacheDexDir = FastdexUtils.getDexCacheDir(project,variantName) + + //copy fastdex-runtime.dex + FileUtils.copyResourceUsingStream(Constants.RUNTIME_DEX_FILENAME,new File(dexOutputDir,Constants.CLASSES_DEX)) + //copy patch.dex + FileUtils.copyFileUsingStream(patchDex,new File(dexOutputDir,"classes2.dex")) + if (FileUtils.fileExists(mergedPatchDex.absolutePath)) { + FileUtils.copyFileUsingStream(mergedPatchDex,new File(dexOutputDir,"classes3.dex")) + FileUtils.copyFileUsingStream(new File(cacheDexDir,Constants.CLASSES_DEX),new File(dexOutputDir,"classes4.dex")) + + int point = 2 + File dexFile = new File(cacheDexDir,"${Constants.CLASSES}${point}${Constants.DEX_SUFFIX}") + while (FileUtils.isLegalFile(dexFile)) { + FileUtils.copyFileUsingStream(dexFile,new File(dexOutputDir,"${Constants.CLASSES}${point + 3}${Constants.DEX_SUFFIX}")) + point++ + dexFile = new File(cacheDexDir,"${Constants.CLASSES}${point}${Constants.DEX_SUFFIX}") + } + } + else { + FileUtils.copyFileUsingStream(new File(cacheDexDir,Constants.CLASSES_DEX),new File(dexOutputDir,"classes3.dex")) + int point = 2 + File dexFile = new File(cacheDexDir,"${Constants.CLASSES}${point}${Constants.DEX_SUFFIX}") + while (FileUtils.isLegalFile(dexFile)) { + FileUtils.copyFileUsingStream(dexFile,new File(dexOutputDir,"${Constants.CLASSES}${point + 2}${Constants.DEX_SUFFIX}")) + point++ + dexFile = new File(cacheDexDir,"${Constants.CLASSES}${point}${Constants.DEX_SUFFIX}") + } + } + printLogWhenDexGenerateComplete(dexOutputDir,false) + } + + /** + * 当dex生成完成后打印日志 + * @param normalBuild + */ + void printLogWhenDexGenerateComplete(File dexOutputDir,boolean normalBuild) { + File cacheDexDir = FastdexUtils.getDexCacheDir(project,variantName) + + //log + StringBuilder sb = new StringBuilder() + sb.append("cached_dex[") + File[] dexFiles = cacheDexDir.listFiles() + for (File file : dexFiles) { + if (file.getName().endsWith(Constants.DEX_SUFFIX)) { + sb.append(file.getName()) + if (file != dexFiles[dexFiles.length - 1]) { + sb.append(",") + } + } + } + sb.append("] cur-dex[") + dexFiles = dexOutputDir.listFiles() + int idx = 0 + for (File file : dexFiles) { + if (file.getName().endsWith(Constants.DEX_SUFFIX)) { + sb.append(file.getName()) + if (idx < (dexFiles.length - 1)) { + sb.append(",") + } + } + idx ++ + } + sb.append("]") + if (normalBuild) { + project.logger.error("==fastdex first build ${sb}") + } + else { + project.logger.error("==fastdex patch build ${sb}") + } + } +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/TransformProxy.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/TransformProxy.groovy new file mode 100644 index 00000000..6ee88ecb --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/transform/TransformProxy.groovy @@ -0,0 +1,35 @@ +package com.dx168.fastdex.build.transform + +import com.android.build.api.transform.QualifiedContent +import com.android.build.api.transform.Transform + +/** + * Created by tong on 17/10/3. + */ +public class TransformProxy extends Transform { + Transform base + + TransformProxy(Transform base) { + this.base = base + } + + @Override + String getName() { + return base.getName() + } + + @Override + Set getInputTypes() { + return base.getInputTypes() + } + + @Override + Set getScopes() { + return base.getScopes() + } + + @Override + boolean isIncremental() { + return base.isIncremental() + } +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ClassInject.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ClassInject.groovy new file mode 100644 index 00000000..8257b1a7 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ClassInject.groovy @@ -0,0 +1,260 @@ +package com.dx168.fastdex.build.util + +import com.android.build.api.transform.DirectoryInput +import com.android.build.api.transform.JarInput +import com.android.build.api.transform.TransformInput +import com.android.build.api.transform.TransformInvocation +import com.dx168.fastdex.build.variant.FastdexVariant +import fastdex.common.utils.FileUtils +import org.objectweb.asm.* +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + source class: + '''''' + public class MainActivity { + + } + ''''' + + dest class: + '''''' + import fastdex.runtime.antilazyload.AntilazyLoad; + + public class MainActivity { + public MainActivity() { + System.out.println(Antilazyload.str); + } + } + '''''' + * 代码注入,往所有的构造方法中添加对fastdex.runtime.antilazyload.AntilazyLoad的依赖 + * Created by tong on 17/10/3. + */ +public class ClassInject implements Opcodes { + /** + * 注入class目录和jar文件 + * @param fastdexVariant + * @param transformInvocation + */ + public static void injectTransformInvocation(FastdexVariant fastdexVariant, TransformInvocation transformInvocation) { + //所有的class目录 + HashSet directoryInputFiles = new HashSet<>(); + //所有输入的jar + HashSet jarInputFiles = new HashSet<>(); + for (TransformInput input : transformInvocation.getInputs()) { + Collection directoryInputs = input.getDirectoryInputs() + if (directoryInputs != null) { + for (DirectoryInput directoryInput : directoryInputs) { + directoryInputFiles.add(directoryInput.getFile()) + } + } + + Collection jarInputs = input.getJarInputs() + if (jarInputs != null) { + for (JarInput jarInput : jarInputs) { + jarInputFiles.add(jarInput.getFile()) + } + } + } + injectDirectoryInputFiles(fastdexVariant,directoryInputFiles) + injectJarInputFiles(fastdexVariant,jarInputFiles) + } + + /** + * 往所有项目代码里注入解决pre-verify问题的code + * @param directoryInputFiles + */ + public static void injectDirectoryInputFiles(FastdexVariant fastdexVariant, HashSet directoryInputFiles) { + def project = fastdexVariant.project + long start = System.currentTimeMillis() + for (File classpathFile : directoryInputFiles) { + project.logger.error("====fastdex ==inject dir: ${classpathFile.getAbsolutePath()}====") + ClassInject.injectDirectory(fastdexVariant,classpathFile,true) + } + long end = System.currentTimeMillis() + project.logger.error("==fastdex inject complete dir-size: ${directoryInputFiles.size()} , use: ${end - start}ms") + } + + /** + * 注入所有的依赖的library输出jar + * + * @param fastdexVariant + * @param directoryInputFiles + */ + public static void injectJarInputFiles(FastdexVariant fastdexVariant, HashSet jarInputFiles) { + def project = fastdexVariant.project + long start = System.currentTimeMillis() + + Set libraryDependencies = fastdexVariant.libraryDependencies + List projectJarFiles = new ArrayList<>() + //获取所有依赖工程的输出jar (compile project(':xxx')) + for (LibDependency dependency : libraryDependencies) { + projectJarFiles.add(dependency.jarFile) + } + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex projectJarFiles : ${projectJarFiles}") + } + for (File file : jarInputFiles) { + if (!projectJarFiles.contains(file)) { + continue + } + project.logger.error("==fastdex ==inject jar: ${file}") + ClassInject.injectJar(fastdexVariant,file,file) + } + long end = System.currentTimeMillis() + project.logger.error("==fastdex inject complete jar-size: ${projectJarFiles.size()} , use: ${end - start}ms") + } + + /** + * 注入jar包 + * @param fastdexVariant + * @param inputJar + * @param outputJar + * @return + */ + public static injectJar(FastdexVariant fastdexVariant, File inputJar,File outputJar) { + File tempDir = new File(fastdexVariant.buildDir,"temp") + FileUtils.deleteDir(tempDir) + FileUtils.ensumeDir(tempDir) + + def project = fastdexVariant.project + project.copy { + from project.zipTree(inputJar) + into tempDir + } + ClassInject.injectDirectory(fastdexVariant,tempDir,false) + project.ant.zip(baseDir: tempDir, destFile: outputJar) + FileUtils.deleteDir(tempDir) +// ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream() +// +// ZipOutputStream outputJarStream = null +// ZipFile zipFile = new ZipFile(file.absolutePath); +// Enumeration enumeration = zipFile.entries(); +// try { +// outputJarStream = new ZipOutputStream(new FileOutputStream(new File("/Users/tong/Desktop/${file.name}"))); +// while (enumeration.hasMoreElements()) { +// ZipEntry entry = (ZipEntry) enumeration.nextElement(); +// if (entry.isDirectory()) { +// continue; +// } +// +// ZipEntry e = new ZipEntry(entry.name) +// outputJarStream.putNextEntry(e) +// //byte[] bytes = FileUtils.readStream(zipFile.getInputStream(entry)) +// byte[] bytes = FileUtils.readContents(new File("/Users/tong/Desktop/a.txt")) +// outputJarStream.write(bytes,0,bytes.length); +// outputJarStream.flush() +// outputJarStream.closeEntry() +// } +// //FileUtils.write2file(zipOutputStream.toByteArray(),file); +// } finally { +// if (outputJarStream != null) { +// outputJarStream.close(); +// } +// +// if (zipFile != null) { +// zipFile.close(); +// } +// } + } + + /** + * 注入指定目录下的所有class + * @param classpath + */ + public static void injectDirectory(FastdexVariant fastdexVariant,File classesDir,boolean applicationProjectSrc) { + if (!FileUtils.dirExists(classesDir.absolutePath)) { + return + } + Path classpath = classesDir.toPath() + + Files.walkFileTree(classpath,new SimpleFileVisitor(){ + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + File classFile = file.toFile() + String fileName = classFile.getName() + if (!fileName.endsWith(Constants.CLASS_SUFFIX)) { + return FileVisitResult.CONTINUE; + } + + boolean needInject = true + if (applicationProjectSrc && (fileName.endsWith("R.class") || fileName.matches("R\\\$\\S{1,}.class"))) { + String packageName = fastdexVariant.getOriginPackageName() + String packageNamePath = packageName.split("\\.").join(File.separator) + if (!classFile.absolutePath.endsWith("${packageNamePath}${File.separator}${fileName}")) { + needInject = false + } + } + if (needInject) { + fastdexVariant.project.logger.error("==fastdex inject: ${classFile.getAbsolutePath()}") + byte[] classBytes = FileUtils.readContents(classFile) + classBytes = ClassInject.inject(classBytes) + FileUtils.write2file(classBytes,classFile) + } + return FileVisitResult.CONTINUE + } + }) + } + + /** + * 往class字节码注入code + * @param classBytes + * @return + */ + public static final byte[] inject(byte[] classBytes) { + ClassReader classReader = new ClassReader(classBytes); + ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); + ClassVisitor classVisitor = new MyClassVisitor(classWriter); + classReader.accept(classVisitor, Opcodes.ASM5); + + return classWriter.toByteArray() + } + + private static class MyClassVisitor extends ClassVisitor { + public MyClassVisitor(ClassVisitor classVisitor) { + super(Opcodes.ASM5, classVisitor); + } + + @Override + public MethodVisitor visitMethod(int access, + String name, + String desc, + String signature, + String[] exceptions) { + if ("".equals(name)) { + //get origin method + MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); + //System.out.println(name + " | " + desc + " | " + signature); + MethodVisitor newMethod = new AsmMethodVisit(mv); + return newMethod; + } else { + return super.visitMethod(access, name, desc, signature, exceptions); + } + } + } + + static class AsmMethodVisit extends MethodVisitor { + public AsmMethodVisit(MethodVisitor mv) { + super(Opcodes.ASM5, mv); + } + + @Override + public void visitInsn(int opcode) { + if (opcode == Opcodes.RETURN) { + super.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;"); + super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + Label l0 = new Label(); + super.visitJumpInsn(IFEQ, l0); + mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + mv.visitFieldInsn(GETSTATIC, "fastdex/runtime/antilazyload/AntilazyLoad", "str", "Ljava/lang/String;"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + super.visitLabel(l0); + } + super.visitInsn(opcode); + } + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/Constants.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/Constants.groovy new file mode 100644 index 00000000..ab816807 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/Constants.groovy @@ -0,0 +1,27 @@ +package com.dx168.fastdex.build.util + +import fastdex.common.ShareConstants; + +/** + * Created by tong on 17/3/14. + */ +public interface Constants extends ShareConstants { + /** + * 最低支持的android gradle build版本 + */ + String MIN_SUPPORT_ANDROID_GRADLE_VERSION = "2.0.0" + String BUILD_DIR = "fastdex" + String INJECTED_JAR_FILENAME = "injected-combined.jar" + String R_TXT = "r.txt" + String RESOURCE_PUBLIC_XML = "public.xml" + String RESOURCE_IDX_XML = "idx.xml" + String RUNTIME_DEX_FILENAME = "fastdex-runtime.dex" + String DEPENDENCIES_FILENAME = "dependencies.json" + String SOURCESET_SNAPSHOOT_FILENAME = "sourceSets.json" + String LAST_DIFF_RESULT_SET_FILENAME = "lastDiffResultSet.json" + String ERROR_REPORT_FILENAME = "last-build-error-report.txt" + String DEFAULT_LIBRARY_VARIANT_DIR_NAME = "release" + + String DEX_MERGE_JAR_FILENAME = "fastdex-dex-merge.jar" + String STUDIO_INFO_SCRIPT_MACOS = "fastdex-studio-info-macos-%s.sh" +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/DexOperation.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/DexOperation.groovy new file mode 100644 index 00000000..282a9d06 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/DexOperation.groovy @@ -0,0 +1,132 @@ +package com.dx168.fastdex.build.util + +import com.android.build.api.transform.Transform +import com.dx168.fastdex.build.variant.FastdexVariant +import fastdex.common.utils.FileUtils +import org.objectweb.asm.* +import com.android.ide.common.blame.Message +import com.android.ide.common.blame.ParsingProcessOutputHandler +import com.android.ide.common.blame.parser.DexParser +import com.android.ide.common.blame.parser.ToolOutputParser +import com.android.ide.common.process.ProcessOutputHandler + +/** + * dex操作 + * Created by tong on 17/11/4. + */ +public class DexOperation implements Opcodes { + /** + * 生成补丁dex + * @param fastdexVariant + * @param base + * @param patchJar + * @param patchDex + */ + public static final void generatePatchDex(FastdexVariant fastdexVariant, Transform base,File patchJar,File patchDex) { + FileUtils.deleteFile(patchDex) + ProcessOutputHandler outputHandler = new ParsingProcessOutputHandler( + new ToolOutputParser(new DexParser(), Message.Kind.ERROR, base.logger), + new ToolOutputParser(new DexParser(), base.logger), + base.androidBuilder.getErrorReporter()) + final List inputFiles = new ArrayList<>() + inputFiles.add(patchJar) + + FileUtils.ensumeDir(patchDex.parentFile) + String androidGradlePluginVersion = GradleUtils.ANDROID_GRADLE_PLUGIN_VERSION + long start = System.currentTimeMillis() + if ("2.0.0".equals(androidGradlePluginVersion)) { + base.androidBuilder.convertByteCode( + inputFiles, + patchDex.parentFile, + false, + null, + base.dexOptions, + null, + false, + true, + outputHandler, + false) + } + else if ("2.1.0".equals(androidGradlePluginVersion) || "2.1.2".equals(androidGradlePluginVersion) || "2.1.3".equals(androidGradlePluginVersion)) { + base.androidBuilder.convertByteCode( + inputFiles, + patchDex.parentFile, + false, + null, + base.dexOptions, + null, + false, + true, + outputHandler) + } + else if (androidGradlePluginVersion.startsWith("2.2.")) { + base.androidBuilder.convertByteCode( + inputFiles, + patchDex.parentFile, + false, + null, + base.dexOptions, + base.getOptimize(), + outputHandler); + } + else if ("2.3.0".equals(androidGradlePluginVersion)) { + base.androidBuilder.convertByteCode( + inputFiles, + patchDex.parentFile, + false, + null,//fix-issue#27 fix-issue#22 + base.dexOptions, + outputHandler) + } + else { + //TODO 补丁的方法数也有可能超过65535个,最好加上使dx生成多个dex的参数,但是一般补丁不会那么大所以暂时不处理 + //调用dx命令 + def process = new ProcessBuilder(FastdexUtils.getDxCmdPath(fastdexVariant.project),"--dex","--output=${patchDex}",patchJar.absolutePath).start() + int status = process.waitFor() + try { + process.destroy() + } catch (Throwable e) { + + } + if (status != 0) { + //拼接生成dex的命令 project.android.getSdkDirectory() + String dxcmd = "${FastdexUtils.getDxCmdPath(fastdexVariant.project)} --dex --output=${patchDex} ${patchJar}" + throw new RuntimeException("==fastdex generate dex fail: \n${dxcmd}") + } + } + + long end = System.currentTimeMillis(); + fastdexVariant.project.logger.error("==fastdex patch transform generate dex success: \n==${patchDex} use: ${end - start}ms") + } + + /** + * 合并dex + * @param fastdexVariant + * @param outputDex 输出的dex路径 + * @param patchDex 补丁dex路径 + * @param cachedDex + */ + public static void mergeDex(FastdexVariant fastdexVariant,File outputDex,File patchDex,File cachedDex) { + long start = System.currentTimeMillis() + def project = fastdexVariant.project + File dexMergeCommandJarFile = new File(FastdexUtils.getBuildDir(project),Constants.DEX_MERGE_JAR_FILENAME) + if (!FileUtils.isLegalFile(dexMergeCommandJarFile)) { + FileUtils.copyResourceUsingStream(Constants.DEX_MERGE_JAR_FILENAME,dexMergeCommandJarFile) + } + + String javaCmdPath = FastdexUtils.getJavaCmdPath() + def process = new ProcessBuilder(javaCmdPath,"-jar",dexMergeCommandJarFile.absolutePath,outputDex.absolutePath,patchDex.absolutePath,cachedDex.absolutePath).start() + int status = process.waitFor() + try { + process.destroy() + } catch (Throwable e) { + + } + if (status != 0) { + String cmd = "${javaCmdPath} -jar ${dexMergeCommandJarFile} ${outputDex} ${patchDex} ${cachedDex}" + throw new RuntimeException("==fastdex merge dex fail: \n${cmd}") + } + long end = System.currentTimeMillis(); + project.logger.error("==fastdex merge dex success: \n==${outputDex} use: ${end - start}ms") + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexBuildListener.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexBuildListener.groovy new file mode 100644 index 00000000..d5d9d732 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexBuildListener.groovy @@ -0,0 +1,231 @@ +package com.dx168.fastdex.build.util + +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.BuildListener; +import org.gradle.BuildResult +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.execution.TaskExecutionListener; +import org.gradle.api.initialization.Settings; +import org.gradle.api.invocation.Gradle; +import org.gradle.api.tasks.TaskState +import org.gradle.util.Clock; +import com.dx168.fastdex.build.FastdexPlugin +import com.github.typ0520.fastdex.Version +import java.lang.management.ManagementFactory +import fastdex.common.utils.FileUtils + +/** + * Created by tong on 17/3/12. + */ +class FastdexBuildListener implements TaskExecutionListener, BuildListener { + private Clock clock + private times = [] + private Project project + + FastdexBuildListener(Project project) { + this.project = project + } + + @Override + void beforeExecute(Task task) { + clock = new org.gradle.util.Clock() + } + + @Override + void afterExecute(Task task, TaskState taskState) { + def ms = clock.timeInMs + times.add([ms, task.path]) + + //task.project.logger.warn "${task.path} spend ${ms}ms" + } + + @Override + void buildStarted(Gradle gradle) {} + + @Override + void projectsEvaluated(Gradle gradle) {} + + @Override + void projectsLoaded(Gradle gradle) {} + + @Override + void settingsEvaluated(Settings settings) {} + + @Override + void buildFinished(BuildResult result) { + if (result.failure == null) { + println "Task spend time:" + for (time in times) { + if (time[0] >= 50) { + printf "%7sms %s\n", time + } + } + } + else { + if (project == null || !project.plugins.hasPlugin("com.android.application")) { + return + } + + Throwable cause = getRootThowable(result.failure) + if (cause == null) { + return + } + + if (cause instanceof FastdexRuntimeException) { + return + } + + StackTraceElement[] stackTrace = cause.getStackTrace() + if (stackTrace == null || stackTrace.length == 0) { + return + } + + StackTraceElement stackTraceElement = stackTrace[0] + if (stackTraceElement == null) { + return + } + + if (stackTraceElement.toString().contains(FastdexPlugin.class.getPackage().getName())) { + File errorLogFile = new File(FastdexUtils.getBuildDir(project),Constants.ERROR_REPORT_FILENAME) + + Map map = getStudioInfo() + + println("\n===========================fastdex error report===========================") + ByteArrayOutputStream bos = new ByteArrayOutputStream() + result.failure.printStackTrace(new PrintStream(bos)) + + String splitStr = "\n\n" + StringBuilder report = new StringBuilder() + //让android studio的Messages窗口显示打开Gradle Console的提示 + report.append("Caused by: ----------------------------------fastdex---------------------------------\n") + report.append("Caused by: Open the Gradle Console in the lower right corner to view the build error report\n") + report.append("Caused by: ${errorLogFile}\n") + report.append("Caused by: ----------------------------------fastdex---------------------------------${splitStr}") + report.append("${new String(bos.toByteArray())}\n") + + String str = "Fastdex build version " + report.append("Fastdex build version : ${Version.FASTDEX_BUILD_VERSION}\n") + report.append("OS : ${getOsName()}\n") + report.append("android_build_version : ${GradleUtils.ANDROID_GRADLE_PLUGIN_VERSION}\n") + report.append("gradle_version : ${project.gradle.gradleVersion}\n") + report.append("buildToolsVersion : ${project.android.getBuildToolsVersion()}\n") + report.append("compileSdkVersion : ${project.android.getCompileSdkVersion()}\n") + report.append("default minSdkVersion : ${project.android.defaultConfig.minSdkVersion.getApiString()}\n") + report.append("default targetSdkVersion : ${project.android.defaultConfig.targetSdkVersion.getApiString()}\n") + report.append("default multiDexEnabled : ${project.android.defaultConfig.multiDexEnabled}\n\n") + + try { + int keyLength = str.length(); + if (!map.isEmpty()) { + for (String key : map.keySet()) { + int dsize = keyLength - key.length(); + report.append(key + getSpaceString(dsize) + ": " + map.get(key) + "\n"); + } + + if (!"true".equals(map.get("instant_run_disabled"))) { + report.append("Fastdex does not support instant run mode, please disable instant run in 'File->Settings...'.\n\n") + } + else { + report.append("\n") + } + } + } catch (Throwable e) { + e.printStackTrace() + } + + report.append("fastdex build exception, welcome to submit issue to us: https://github.com/typ0520/fastdex/issues") + System.err.println(report.toString()) + System.err.println("${errorLogFile}") + + int idx = report.indexOf(splitStr) + String content = report.toString() + if (idx != -1 && (idx + splitStr.length()) < content.length()) { + content = content.substring(idx + splitStr.length()) + } + FileUtils.write2file(content.getBytes(),errorLogFile) + println("\n===========================fastdex error report===========================") + } + } + } + + String getOsName() { + try { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH) + } catch (Throwable e) { + + } + return "" + } + + Throwable getRootThowable(Throwable throwable) { + return throwable.cause != null ? getRootThowable(throwable.cause) : throwable + } + + public Map getStudioInfo() { + Map map = new HashMap<>() + if (Os.isFamily(Os.FAMILY_MAC)) { + try { + File script = new File(FastdexUtils.getBuildDir(project),String.format(Constants.STUDIO_INFO_SCRIPT_MACOS,Version.FASTDEX_BUILD_VERSION)) + if (!FileUtils.isLegalFile(script)) { + FileUtils.copyResourceUsingStream(Constants.STUDIO_INFO_SCRIPT_MACOS,script) + } + + int pid = getPid(); + if (pid == -1) { + return map; + } + + Process process = new ProcessBuilder("sh",script.getAbsolutePath(),"${pid}").start(); + int status = process.waitFor(); + if (status == 0) { + byte[] bytes = FileUtils.readStream(process.getInputStream()); + String response = new String(bytes); + BufferedReader reader = new BufferedReader(new StringReader(response)); + System.out.println(); + String line = null; + while ((line = reader.readLine()) != null) { + System.out.println(line); + String[] arr = line.split("="); + if (arr != null && arr.length == 2) { + map.put(arr[0],arr[1]); + } + } + } + process.destroy(); + } catch (Throwable e) { + //e.printStackTrace() + } + } + return map + } + + public static String getSpaceString(int count) { + if (count > 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(" "); + } + return sb.toString(); + } + return ""; + } + + public static int getPid() { + String name = ManagementFactory.getRuntimeMXBean().getName(); + if (name != null) { + String[] arr = name.split("@"); + try { + return Integer.valueOf(arr[0]); + } catch (Throwable e) { + + } + } + return -1; + } + + public static void addByProject(Project pro) { + FastdexBuildListener listener = new FastdexBuildListener(pro) + pro.gradle.addListener(listener) + } +} \ No newline at end of file diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexRuntimeException.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexRuntimeException.groovy new file mode 100644 index 00000000..90b691af --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexRuntimeException.groovy @@ -0,0 +1,26 @@ +package com.dx168.fastdex.build.util + +/** + * Created by tong on 17/4/18. + */ +public class FastdexRuntimeException extends RuntimeException { + + FastdexRuntimeException() { + } + + FastdexRuntimeException(String var1) { + super(var1) + } + + FastdexRuntimeException(String var1, Throwable var2) { + super(var1, var2) + } + + FastdexRuntimeException(Throwable var1) { + super(var1) + } + + FastdexRuntimeException(String var1, Throwable var2, boolean var3, boolean var4) { + super(var1, var2, var3, var4) + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexUtils.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexUtils.groovy new file mode 100644 index 00000000..8c6ee7f7 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/FastdexUtils.groovy @@ -0,0 +1,319 @@ +package com.dx168.fastdex.build.util + +import fastdex.common.ShareConstants +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.Project +import fastdex.common.utils.FileUtils + +/** + * Created by tong on 17/3/14. + */ +public class FastdexUtils { + /** + * 获取sdk路径 + * @param project + * @return + */ + public static final String getSdkDirectory(Project project) { + String sdkDirectory = project.android.getSdkDirectory() + if (sdkDirectory.contains("\\")) { + sdkDirectory = sdkDirectory.replace("\\", "/"); + } + return sdkDirectory + } + + /** + * 获取dx命令路径 + * @param project + * @return + */ + public static final String getDxCmdPath(Project project) { + File dx = new File(FastdexUtils.getSdkDirectory(project),"build-tools${File.separator}${project.android.getBuildToolsVersion()}${File.separator}dx") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return "${dx.absolutePath}.bat" + } + return dx.getAbsolutePath() + } + + /** + * 获取aapt命令路径 + * @param project + * @return + */ + public static final String getAaptCmdPath(Project project) { + File aapt = new File(FastdexUtils.getSdkDirectory(project),"build-tools${File.separator}${project.android.getBuildToolsVersion()}${File.separator}aapt") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return "${aapt.absolutePath}.exe" + } + return aapt.getAbsolutePath() + } + + /** + * 获取adb命令路径 + * @param project + * @return + */ + public static final String getAdbCmdPath(Project project) { + File adb = new File(FastdexUtils.getSdkDirectory(project),"platform-tools${File.separator}adb") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return "${adb.absolutePath}.exe" + } + return adb.getAbsolutePath() + } + + /** + * 获取当前jdk路径 + * @return + */ + public static final String getCurrentJdk() { + String javaHomeProp = System.properties.'java.home' + if (javaHomeProp) { + int jreIndex = javaHomeProp.lastIndexOf("${File.separator}jre") + if (jreIndex != -1) { + return javaHomeProp.substring(0, jreIndex) + } else { + return javaHomeProp + } + } else { + return System.getenv("JAVA_HOME") + } + } + + /** + * 获取java命令路径 + * @return + */ + public static final String getJavaCmdPath() { + StringBuilder cmd = new StringBuilder(getCurrentJdk()) + if (!cmd.toString().endsWith(File.separator)) { + cmd.append(File.separator) + } + cmd.append("bin${File.separator}java") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cmd.append(".exe") + } + return new File(cmd.toString()).absolutePath + } + + /** + * 是否存在dex缓存 + * @param project + * @param variantName + * @return + */ + public static boolean hasDexCache(Project project, String variantName) { + File cacheDexDir = getDexCacheDir(project,variantName) + return FileUtils.hasDex(cacheDexDir) + } + + /** + * 获取fastdex的build目录 + * @param project + * @return + */ + public static final File getBuildDir(Project project) { + File file = new File(project.getBuildDir(),Constants.BUILD_DIR); + return file; + } + + /** + * 获取fastdex指定variantName的build目录 + * @param project + * @return + */ + public static final File getBuildDir(Project project,String variantName) { + File file = new File(getBuildDir(project),variantName); + return file; + } + + /** + * 获取fastdex指定variantName的work目录 + * @param project + * @return + */ + public static final File getWorkDir(Project project,String variantName) { + File file = new File(getBuildDir(project,variantName),"work") + return file; + } + + /** + * 获取dex目录 + * @param project + * @param variantName + * @return + */ + public static getDexDir(Project project,String variantName) { + File file = new File(getBuildDir(project,variantName),"dex"); + return file; + } + + /** + * 获取指定variantName的dex缓存目录 + * @param project + * @return + */ + public static final File getDexCacheDir(Project project,String variantName) { + File file = new File(getDexDir(project,variantName),"cache"); + return file; + } + + /** + * 获取指定variantName的已合并的补丁dex目录 + * @param project + * @return + */ + public static final File getMergedPatchDexDir(Project project,String variantName) { + File file = new File(getDexDir(project,variantName),"merged-patch"); + return file; + } + + /** + * 获取指定variantName的补丁dex目录 + * @param project + * @return + */ + public static final File getPatchDexDir(Project project,String variantName) { + File file = new File(getDexDir(project,variantName),"patch"); + return file; + } + + /** + * 获取指定variantName的补丁dex文件 + * @param project + * @return + */ + public static final File getPatchDexFile(Project project,String variantName) { + File file = new File(getPatchDexDir(project,variantName),Constants.CLASSES_DEX); + return file; + } + + /** + * 获取指定variantName的补丁merged-dex文件 + * @param project + * @param variantName + * @return + */ + public static final File getMergedPatchDex(Project project,String variantName) { + File file = new File(getMergedPatchDexDir(project,variantName),Constants.CLASSES_DEX); + return file; + } + + /** + * 获取指定variantName的源码目录快照 + * @param project + * @return + */ + public static final File getSourceSetSnapshootFile(Project project, String variantName) { + File file = new File(getBuildDir(project,variantName),Constants.SOURCESET_SNAPSHOOT_FILENAME); + return file; + } + + /** + * 清空所有缓存 + * @param project + * @param variantName + * @return + */ + public static boolean cleanCache(Project project,String variantName) { + File dir = getBuildDir(project,variantName) + project.logger.error("==fastdex clean dir: ${dir}") + return FileUtils.deleteDir(dir) + } + + /** + * 清空指定variantName缓存 + * @param project + * @param variantName + * @return + */ + public static boolean cleanAllCache(Project project) { + File dir = getBuildDir(project) + project.logger.error("==fastdex clean dir: ${dir}") + return FileUtils.deleteDir(dir) + } + + /** + * 获取资源映射文件 + * @param project + * @param variantName + * @return + */ + public static File getResourceMappingFile(Project project, String variantName) { + File resourceMappingFile = new File(getBuildResourceDir(project,variantName),Constants.R_TXT) + return resourceMappingFile + } + + public static File getResourceDir(Project project, String variantName) { + File resDir = new File(getBuildDir(project,variantName),"res") + return resDir + } + + public static File getResourcesApk(Project project, String variantName) { + File resourcesApk = new File(getResourceDir(project,variantName),ShareConstants.RESOURCE_APK_FILE_NAME) + return resourcesApk + } + + /** + * 获取缓存的idx.xml文件 + * @param project + * @param variantName + * @return + */ + public static File getIdxXmlFile(Project project, String variantName) { + File idxXmlFile = new File(getBuildResourceDir(project,variantName),Constants.RESOURCE_IDX_XML) + return idxXmlFile + } + + /** + * 获取缓存的public.xml文件 + * @param project + * @param variantName + * @return + */ + public static File getPublicXmlFile(Project project, String variantName) { + File publicXmlFile = new File(getBuildResourceDir(project,variantName),Constants.RESOURCE_PUBLIC_XML) + return publicXmlFile + } + + private static File getBuildResourceDir(Project project, String variantName) { + return new File(getBuildDir(project,variantName),"r") + } + + /** + * 获取全量打包时的依赖列表 + * @param project + * @param variantName + * @return + */ + public static File getCachedDependListFile(Project project,String variantName) { + File cachedDependListFile = new File(getBuildDir(project,variantName),Constants.DEPENDENCIES_FILENAME) + return cachedDependListFile + } + + public static File getMetaInfoFile(Project project,String variantName) { + File cachedDependListFile = new File(getBuildDir(project,variantName),Constants.META_INFO_FILENAME) + return cachedDependListFile + } + + /** + * 获取缓存的java文件对比结果文件 + * @param project + * @param variantName + * @return + */ + public static File getDiffResultSetFile(Project project,String variantName) { + File diffResultFile = new File(getBuildDir(project,variantName),Constants.LAST_DIFF_RESULT_SET_FILENAME) + return diffResultFile + } + + /** + * 获取全量打包时的包括所有代码的jar包 + * @param project + * @param variantName + * @return + */ + public static File getInjectedJarFile(Project project,String variantName) { + File injectedJarFile = new File(getBuildDir(project,variantName),Constants.INJECTED_JAR_FILENAME) + return injectedJarFile + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/GradleUtils.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/GradleUtils.groovy new file mode 100644 index 00000000..4d66ede6 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/GradleUtils.groovy @@ -0,0 +1,314 @@ +package com.dx168.fastdex.build.util + +import com.android.build.api.transform.Format +import com.android.build.gradle.internal.pipeline.TransformManager +import com.android.builder.model.Version +import com.google.common.collect.Lists +import com.android.build.gradle.internal.transforms.JarMerger +import fastdex.common.utils.FileUtils +import groovy.xml.QName +import org.gradle.api.GradleException +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import com.android.build.api.transform.DirectoryInput +import com.android.build.api.transform.JarInput +import com.android.build.api.transform.QualifiedContent +import com.android.build.api.transform.Status +import com.android.build.api.transform.TransformInput +import com.android.build.api.transform.TransformInvocation +import com.android.build.gradle.internal.pipeline.TransformInvocationBuilder +import com.google.common.collect.ImmutableList +import com.android.build.api.transform.Transform +import org.gradle.api.Project + +/** + * Created by tong on 17/3/14. + */ +public class GradleUtils { + public static final String ANDROID_GRADLE_PLUGIN_VERSION = Version.ANDROID_GRADLE_PLUGIN_VERSION + + /** + * 获取指定variant的依赖列表 + * @param project + * @param applicationVariant + * @return + */ + public static Set getCurrentDependList(Project project,Object applicationVariant) { + String buildTypeName = applicationVariant.getBuildType().buildType.getName() + + Set result = new HashSet<>() + + project.configurations.compile.each { File file -> + //project.logger.error("==fastdex compile: ${file.absolutePath}") + result.add(file.getAbsolutePath()) + } + + project.configurations."${buildTypeName}Compile".each { File file -> + //project.logger.error("==fastdex ${buildTypeName}Compile: ${file.absolutePath}") + result.add(file.getAbsolutePath()) + } + +// project.configurations.all.findAll { !it.allDependencies.empty }.each { c -> +// if (c.name.toString().equals("compile") +// || c.name.toString().equals("apt") +// || c.name.toString().equals("_${buildTypeName}Compile".toString())) { +// c.allDependencies.each { dep -> +// String depStr = "$dep.group:$dep.name:$dep.version" +// if (!"null:unspecified:null".equals(depStr)) { +// result.add(depStr) +// } +// } +// } +// } + return result + } + + /** + * 获取transformClassesWithDexFor${variantName}任务的dex输出目录 + * @param transformInvocation + * @return + */ + public static File getDexOutputDir(Project project,Transform realTransform,TransformInvocation transformInvocation) { + def outputProvider = transformInvocation.getOutputProvider() + def outputDir = null + String androidGradlePluginVersion = ANDROID_GRADLE_PLUGIN_VERSION + + if (androidGradlePluginVersion.startsWith("2.4.")) { + outputDir = outputProvider.getContentLocation( + "main", + realTransform.getOutputTypes(), + TransformManager.SCOPE_FULL_PROJECT, + Format.DIRECTORY) + + return outputDir + } + + List jarInputs = Lists.newArrayList(); + List directoryInputs = Lists.newArrayList(); + for (TransformInput input : transformInvocation.getInputs()) { + jarInputs.addAll(input.getJarInputs()); + directoryInputs.addAll(input.getDirectoryInputs()); + } + + if (androidGradlePluginVersion.compareTo("2.3.0") < 0) { + //2.3.0以前的版本 + if ((jarInputs.size() + directoryInputs.size()) == 1 + || !realTransform.dexOptions.getPreDexLibraries()) { + outputDir = outputProvider.getContentLocation("main", + realTransform.getOutputTypes(), realTransform.getScopes(), + Format.DIRECTORY); + } + else { + outputDir = outputProvider.getContentLocation("main", + TransformManager.CONTENT_DEX, realTransform.getScopes(), + Format.DIRECTORY); + } + } + else { + //2.3.0以后的版本包括2.3.0 + if ((jarInputs.size() + directoryInputs.size()) == 1 + || !realTransform.dexOptions.getPreDexLibraries()) { + outputDir = outputProvider.getContentLocation("main", + realTransform.getOutputTypes(), + TransformManager.SCOPE_FULL_PROJECT, + Format.DIRECTORY); + } + else { + outputDir = outputProvider.getContentLocation("main", + TransformManager.CONTENT_DEX, TransformManager.SCOPE_FULL_PROJECT, + Format.DIRECTORY); + } + } + return outputDir; + } + + /** + * 获取AndroidManifest.xml文件package属性值 + * @param manifestPath + * @return + */ + public static String getPackageName(String manifestPath) { + def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8")) + String packageName = xml.attribute('package') + + return packageName + } + + /** + * 获取启动的activity + * @param manifestPath + * @return + */ + public static String getBootActivity(String manifestPath) { + def bootActivityName = "" + def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8")) + def application = xml.application[0] + + if (application) { + def activities = application.activity + QName androidNameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android'); + + try { + activities.each { activity-> + def activityName = activity.attribute(androidNameAttr) + + if (activityName) { + def intentFilters = activity."intent-filter" + if (intentFilters) { + intentFilters.each { intentFilter-> + def actions = intentFilter.action + def categories = intentFilter.category + if (actions && categories) { + //android.intent.action.MAIN + //android.intent.category.LAUNCHER + + boolean hasMainAttr = false + boolean hasLauncherAttr = false + + actions.each { action -> + def attr = action.attribute(androidNameAttr) + if ("android.intent.action.MAIN".equals(attr.toString())) { + hasMainAttr = true + } + } + + categories.each { categoriy -> + def attr = categoriy.attribute(androidNameAttr) + if ("android.intent.category.LAUNCHER".equals(attr.toString())) { + hasLauncherAttr = true + } + } + + if (hasMainAttr && hasLauncherAttr) { + bootActivityName = activityName + throw new JumpException() + } + } + } + } + } + } + } catch (JumpException e) { + + } + } + return bootActivityName + } + + /** + * 合并所有的代码到一个jar钟 + * @param project + * @param transformInvocation + * @param outputJar 输出路径 + */ + public static void executeMerge(Project project,TransformInvocation transformInvocation, File outputJar) { + List jarInputs = Lists.newArrayList(); + List dirInputs = Lists.newArrayList(); + + for (TransformInput input : transformInvocation.getInputs()) { + jarInputs.addAll(input.getJarInputs()); + } + + for (TransformInput input : transformInvocation.getInputs()) { + dirInputs.addAll(input.getDirectoryInputs()); + } + + JarMerger jarMerger = getClassJarMerger(outputJar) + jarInputs.each { jar -> + project.logger.error("==fastdex merge jar " + jar.getFile()) + jarMerger.addJar(jar.getFile()) + } + dirInputs.each { dir -> + project.logger.error("==fastdex merge dir " + dir) + jarMerger.addFolder(dir.getFile()) + } + jarMerger.close() + if (!FileUtils.isLegalFile(outputJar)) { + throw new GradleException("merge jar fail: \n jarInputs: ${jarInputs}\n dirInputs: ${dirInputs}\n mergedJar: ${outputJar}") + } + project.logger.error("==fastdex merge jar success: ${outputJar}") + } + + private static JarMerger getClassJarMerger(File jarFile) { + JarMerger jarMerger = new JarMerger(jarFile) + + Class zipEntryFilterClazz + try { + zipEntryFilterClazz = Class.forName("com.android.builder.packaging.ZipEntryFilter") + } catch (Throwable t) { + zipEntryFilterClazz = Class.forName("com.android.builder.signing.SignedJarBuilder\$IZipEntryFilter") + } + + Class[] classArr = new Class[1]; + classArr[0] = zipEntryFilterClazz + InvocationHandler handler = new InvocationHandler(){ + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + return args[0].endsWith(Constants.CLASS_SUFFIX); + } + }; + Object proxy = Proxy.newProxyInstance(zipEntryFilterClazz.getClassLoader(), classArr, handler); + + jarMerger.setFilter(proxy); + + return jarMerger + } + + public static TransformInvocation createNewTransformInvocation(Transform transform,TransformInvocation transformInvocation,File inputJar) { + TransformInvocationBuilder builder = new TransformInvocationBuilder(transformInvocation.getContext()); + builder.addInputs(jarFileToInputs(transform,inputJar)) + builder.addOutputProvider(transformInvocation.getOutputProvider()) + builder.addReferencedInputs(transformInvocation.getReferencedInputs()) + builder.addSecondaryInputs(transformInvocation.getSecondaryInputs()) + builder.setIncrementalMode(transformInvocation.isIncremental()) + + return builder.build() + } + + /** + * change the jar file to TransformInputs + */ + private static Collection jarFileToInputs(Transform transform,File jarFile) { + TransformInput transformInput = new TransformInput() { + @Override + Collection getJarInputs() { + JarInput jarInput = new JarInput() { + @Override + Status getStatus() { + return Status.ADDED + } + + @Override + String getName() { + return jarFile.getName().substring(0, + jarFile.getName().length() - ".jar".length()) + } + + @Override + File getFile() { + return jarFile + } + + @Override + Set getContentTypes() { + return transform.getInputTypes() + } + + @Override + Set getScopes() { + return transform.getScopes() + } + } + return ImmutableList.of(jarInput) + } + + + @Override + Collection getDirectoryInputs() { + return ImmutableList.of() + } + } + return ImmutableList.of(transformInput) + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JarOperation.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JarOperation.groovy new file mode 100644 index 00000000..e5be2958 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JarOperation.groovy @@ -0,0 +1,202 @@ +package com.dx168.fastdex.build.util + +import com.android.build.api.transform.DirectoryInput +import com.android.build.api.transform.JarInput +import com.android.build.api.transform.TransformInput +import com.android.build.api.transform.TransformInvocation +import fastdex.build.lib.snapshoot.api.DiffResultSet +import com.dx168.fastdex.build.variant.FastdexVariant +import fastdex.common.utils.FileUtils +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.GradleException +import org.objectweb.asm.Opcodes +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * jar操作 + * Created by tong on 17/11/4. + */ +public class JarOperation implements Opcodes { + public static void generatePatchJar(FastdexVariant fastdexVariant, TransformInvocation transformInvocation, File patchJar) throws IOException { + Set libraryDependencies = fastdexVariant.libraryDependencies + Map jarAndProjectPathMap = new HashMap<>() + List projectJarFiles = new ArrayList<>() + //获取所有依赖工程的输出jar (compile project(':xxx')) + for (LibDependency dependency : libraryDependencies) { + projectJarFiles.add(dependency.jarFile) + jarAndProjectPathMap.put(dependency.jarFile.absolutePath,dependency.dependencyProject.projectDir.absolutePath) + } + + //所有的class目录 + Set directoryInputFiles = new HashSet<>(); + //所有输入的jar + Set jarInputFiles = new HashSet<>(); + for (TransformInput input : transformInvocation.getInputs()) { + Collection directoryInputs = input.getDirectoryInputs() + if (directoryInputs != null) { + for (DirectoryInput directoryInput : directoryInputs) { + directoryInputFiles.add(directoryInput.getFile()) + } + } + + if (!projectJarFiles.isEmpty()) { + Collection jarInputs = input.getJarInputs() + if (jarInputs != null) { + for (JarInput jarInput : jarInputs) { + if (projectJarFiles.contains(jarInput.getFile())) { + jarInputFiles.add(jarInput.getFile()) + } + } + } + } + } + + def project = fastdexVariant.project + File tempDir = new File(fastdexVariant.buildDir,"temp") + FileUtils.deleteDir(tempDir) + FileUtils.ensumeDir(tempDir) + + Set moudleDirectoryInputFiles = new HashSet<>() + DiffResultSet diffResultSet = fastdexVariant.projectSnapshoot.diffResultSet + for (File file : jarInputFiles) { + String projectPath = jarAndProjectPathMap.get(file.absolutePath) + List patterns = diffResultSet.addOrModifiedClassesMap.get(projectPath) + if (patterns != null && !patterns.isEmpty()) { + File classesDir = new File(tempDir,"${file.name}-${System.currentTimeMillis()}") + project.copy { + from project.zipTree(file) + for (String pattern : patterns) { + include pattern + } + into classesDir + } + moudleDirectoryInputFiles.add(classesDir) + directoryInputFiles.add(classesDir) + } + } + JarOperation.generatePatchJar(fastdexVariant,directoryInputFiles,moudleDirectoryInputFiles,patchJar); + } + + /** + * 生成补丁jar,仅把变化部分参与jar的生成 + * @param project + * @param directoryInputFiles + * @param outputJar + * @param changedClassPatterns + * @throws IOException + */ + public static void generatePatchJar(FastdexVariant fastdexVariant, Set directoryInputFiles,Set moudleDirectoryInputFiles, File patchJar) throws IOException { + long start = System.currentTimeMillis() + def project = fastdexVariant.project + project.logger.error("==fastdex generate patch jar start") + + if (directoryInputFiles == null || directoryInputFiles.isEmpty()) { + throw new IllegalArgumentException("DirectoryInputFiles can not be null!!") + } + + Set changedClasses = fastdexVariant.projectSnapshoot.diffResultSet.addOrModifiedClasses + if (fastdexVariant.configuration.hotClasses != null && fastdexVariant.configuration.hotClasses.length > 0) { + String packageName = fastdexVariant.getOriginPackageName() + for (String str : fastdexVariant.configuration.hotClasses) { + if (str != null) { + changedClasses.add(str.replaceAll("\\{package\\}",packageName)) + } + } + } + + if (project.fastdex.debug) { + project.logger.error("==fastdex debug changedClasses: ${changedClasses}") + } + + if (changedClasses == null || changedClasses.isEmpty()) { + throw new IllegalArgumentException("No java files changed!!") + } + + FileUtils.deleteFile(patchJar) + + boolean willExeDexMerge = fastdexVariant.willExecDexMerge() + + ZipOutputStream outputJarStream = null + try { + outputJarStream = new ZipOutputStream(new FileOutputStream(patchJar)); + for (File classpathFile : directoryInputFiles) { + Path classpath = classpathFile.toPath() + + boolean skip = (moudleDirectoryInputFiles != null && moudleDirectoryInputFiles.contains(classpathFile)) + + Files.walkFileTree(classpath,new SimpleFileVisitor(){ + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!file.toFile().getName().endsWith(Constants.CLASS_SUFFIX)) { + return FileVisitResult.CONTINUE; + } + Path relativePath = classpath.relativize(file) + String entryName = relativePath.toString() + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + entryName = entryName.replace("\\", "/"); + } + + if (skip) { + ZipEntry e = new ZipEntry(entryName) + outputJarStream.putNextEntry(e) + + if (project.fastdex.debug) { + project.logger.error("==fastdex add entry: ${e}") + } + byte[] bytes = FileUtils.readContents(file.toFile()) + //如果需要触发dex merge,必须注入代码 + if (willExeDexMerge) { + bytes = ClassInject.inject(bytes) + project.logger.error("==fastdex inject: ${entryName}") + } + outputJarStream.write(bytes,0,bytes.length) + outputJarStream.closeEntry() + } + else { + String className = relativePath.toString().substring(0,relativePath.toString().length() - Constants.CLASS_SUFFIX.length()); + className = className.replaceAll(Os.isFamily(Os.FAMILY_WINDOWS) ? "\\\\" : File.separator,"\\.") + for (String cn : changedClasses) { + if (cn.equals(className) || className.startsWith("${cn}\$")) { + + ZipEntry e = new ZipEntry(entryName) + outputJarStream.putNextEntry(e) + + if (project.fastdex.debug) { + project.logger.error("==fastdex add entry: ${e}") + } + byte[] bytes = FileUtils.readContents(file.toFile()) + //如果需要触发dex merge,必须注入代码 + if (willExeDexMerge) { + bytes = ClassInject.inject(bytes) + project.logger.error("==fastdex inject: ${entryName}") + } + outputJarStream.write(bytes,0,bytes.length) + outputJarStream.closeEntry() + break; + } + } + } + return FileVisitResult.CONTINUE + } + }) + } + + } finally { + if (outputJarStream != null) { + outputJarStream.close(); + } + } + + if (!FileUtils.isLegalFile(patchJar)) { + throw new GradleException("==fastdex generate patch jar fail: ${patchJar}") + } + long end = System.currentTimeMillis(); + project.logger.error("==fastdex generate patch jar complete: ${patchJar} use: ${end - start}ms") + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JumpException.java b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JumpException.java new file mode 100644 index 00000000..387ee0c1 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/JumpException.java @@ -0,0 +1,14 @@ +package com.dx168.fastdex.build.util; + +/** + * Created by tong on 17/5/2. + */ + +public class JumpException extends RuntimeException { + public JumpException() { + } + + public JumpException(String s) { + super(s); + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/LibDependency.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/LibDependency.groovy new file mode 100644 index 00000000..d8231c94 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/LibDependency.groovy @@ -0,0 +1,207 @@ +package com.dx168.fastdex.build.util; + +import com.android.build.gradle.api.ApplicationVariant; +import com.android.build.gradle.internal.dependency.VariantDependencies +import com.android.builder.model.AndroidLibrary +import org.gradle.api.Project +import org.gradle.platform.base.Library; + +/** + * Created by tong on 17/4/15. + */ +public class LibDependency { + public final File jarFile; + public final Project dependencyProject; + public final boolean androidLibrary; + + LibDependency(File jarFile, Project dependencyProject, boolean androidLibrary) { + this.jarFile = jarFile + this.dependencyProject = dependencyProject + this.androidLibrary = androidLibrary + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + LibDependency that = (LibDependency) o + + if (jarFile != that.jarFile) return false + + return true + } + + int hashCode() { + return (jarFile != null ? jarFile.hashCode() : 0) + } + + @Override + public String toString() { + return "LibDependency{" + + "jarFile=" + jarFile + + ", dependencyProject=" + dependencyProject + + ", androidLibrary=" + androidLibrary + + '}'; + } + + private static Project getProjectByPath(Collection allprojects, String path) { + return allprojects.find { it.path.equals(path) } + } + + /** + * 扫描依赖(<= 2.3.0) + * @param library + * @param libraryDependencies + */ + private static final void scanDependency(com.android.builder.model.Library library,Set libraryDependencies) { + if (library == null) { + return + } + if (library.getProject() == null) { + return + } + if (libraryDependencies.contains(library)) { + return + } + + libraryDependencies.add(library) + + if (library instanceof com.android.builder.model.AndroidLibrary) { + List libraryList = library.getJavaDependencies() + if (libraryList != null) { + for (com.android.builder.model.Library item : libraryList) { + scanDependency(item,libraryDependencies) + } + } + + libraryList = library.getLibraryDependencies() + if (libraryList != null) { + for (com.android.builder.model.Library item : libraryList) { + scanDependency(item,libraryDependencies) + } + } + } + else if (library instanceof com.android.builder.model.JavaLibrary) { + List libraryList = library.getDependencies() + + if (libraryList != null) { + for (com.android.builder.model.Library item : libraryList) { + scanDependency(item,libraryDependencies) + } + } + } + } + + /** + * 扫描依赖(2.0.0 <= android-build-version <= 2.2.0) + * @param library + * @param libraryDependencies + */ + private static final void scanDependency_2_0_0(Object library,Set libraryDependencies) { + if (library == null) { + return + } + + if (library.getProject() == null){ + return + } + if (libraryDependencies.contains(library)) { + return + } + + libraryDependencies.add(library) + + if (library instanceof com.android.builder.model.AndroidLibrary) { + List libraryList = library.getLibraryDependencies() + if (libraryList != null) { + for (com.android.builder.model.Library item : libraryList) { + scanDependency_2_0_0(item,libraryDependencies) + } + } + } + } + + /** + * 解析项目的工程依赖 compile project('xxx') + * @param project + * @return + */ + public static final Set resolveProjectDependency(Project project, ApplicationVariant apkVariant) { + Set libraryDependencySet = new HashSet<>() + VariantDependencies variantDeps = apkVariant.getVariantData().getVariantDependency(); + if (GradleUtils.ANDROID_GRADLE_PLUGIN_VERSION.compareTo("2.3.0") >= 0) { + def allDependencies = new HashSet<>() + allDependencies.addAll(variantDeps.getCompileDependencies().getAllJavaDependencies()) + allDependencies.addAll(variantDeps.getCompileDependencies().getAllAndroidDependencies()) + + for (Object dependency : allDependencies) { + if (dependency.projectPath != null) { + def dependencyProject = getProjectByPath(project.rootProject.allprojects,dependency.projectPath); + boolean androidLibrary = dependency.getClass().getName().equals("com.android.builder.dependency.level2.AndroidDependency"); + File jarFile = null + if (androidLibrary) { + jarFile = dependency.getJarFile() + } + else { + jarFile = dependency.getArtifactFile() + } + LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,androidLibrary) + libraryDependencySet.add(libraryDependency) + } + } + } + else if (GradleUtils.ANDROID_GRADLE_PLUGIN_VERSION.compareTo("2.2.0") >= 0) { + Set librarySet = new HashSet<>() + for (Object jarLibrary : variantDeps.getCompileDependencies().getJarDependencies()) { + scanDependency(jarLibrary,librarySet) + } + for (Object androidLibrary : variantDeps.getCompileDependencies().getAndroidDependencies()) { + scanDependency(androidLibrary,librarySet) + } + + for (com.android.builder.model.Library library : librarySet) { + boolean isAndroidLibrary = (library instanceof AndroidLibrary); + File jarFile = null + def dependencyProject = getProjectByPath(project.rootProject.allprojects,library.getProject()); + if (isAndroidLibrary) { + com.android.builder.dependency.LibraryDependency androidLibrary = library; + jarFile = androidLibrary.getJarFile() + } + else { + jarFile = library.getJarFile(); + } + LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,isAndroidLibrary) + libraryDependencySet.add(libraryDependency) + } + } + else { + Set librarySet = new HashSet<>() + for (Object jarLibrary : variantDeps.getJarDependencies()) { + if (jarLibrary.getProjectPath() != null) { + librarySet.add(jarLibrary) + } + //scanDependency_2_0_0(jarLibrary,librarySet) + } + for (Object androidLibrary : variantDeps.getAndroidDependencies()) { + scanDependency_2_0_0(androidLibrary,librarySet) + } + + for (Object library : librarySet) { + boolean isAndroidLibrary = (library instanceof AndroidLibrary); + File jarFile = null + def projectPath = (library instanceof com.android.builder.dependency.JarDependency) ? library.getProjectPath() : library.getProject() + def dependencyProject = getProjectByPath(project.rootProject.allprojects,projectPath); + if (isAndroidLibrary) { + com.android.builder.dependency.LibraryDependency androidLibrary = library; + jarFile = androidLibrary.getJarFile() + } + else { + jarFile = library.getJarFile(); + } + LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,isAndroidLibrary) + libraryDependencySet.add(libraryDependency) + } + } + return libraryDependencySet + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/MetaInfo.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/MetaInfo.groovy new file mode 100644 index 00000000..18729856 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/MetaInfo.groovy @@ -0,0 +1,68 @@ +package com.dx168.fastdex.build.util + +import fastdex.common.utils.SerializeUtils +import com.dx168.fastdex.build.variant.FastdexVariant +import com.google.gson.Gson +import fastdex.common.utils.FileUtils +import org.gradle.api.Project; + +/** + * Created by tong on 17/4/18. + */ +public class MetaInfo { + /** + * 全量编译时的工程路径 + */ + public String projectPath + + public String rootProjectPath + + public String fastdexVersion + /** + * 全量编译完成后输出的dex个数 + */ + public int dexCount + + /** + * 全量编译完成的时间 + */ + public long buildMillis + + public String variantName + + public int mergedDexVersion + + public int patchDexVersion + + + /** + * 是否移动了工程目录 + * @param project + * @return + */ + public boolean isRootProjectDirChanged(String curRootProjectPath) { + return !curRootProjectPath.equals(rootProjectPath) + } + + public void save(FastdexVariant fastdexVariant) { + File metaInfoFile = FastdexUtils.getMetaInfoFile(fastdexVariant.project,fastdexVariant.variantName) + SerializeUtils.serializeTo(new FileOutputStream(metaInfoFile),this) + } + + public static MetaInfo load(Project project,String variantName) { + File metaInfoFile = FastdexUtils.getMetaInfoFile(project,variantName) + try { + return new Gson().fromJson(new String(FileUtils.readContents(metaInfoFile)),MetaInfo.class) + } catch (Throwable e) { + e.printStackTrace() + } + } + + @Override + public String toString() { + return "MetaInfo{" + + "buildMillis=" + buildMillis + + ", variantName='" + variantName + '\'' + + '}'; + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ProjectSnapshoot.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ProjectSnapshoot.groovy new file mode 100644 index 00000000..0a7c76f5 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/ProjectSnapshoot.groovy @@ -0,0 +1,343 @@ +package com.dx168.fastdex.build.util + +import fastdex.build.lib.snapshoot.sourceset.JavaDirectorySnapshoot +import fastdex.build.lib.snapshoot.sourceset.SourceSetDiffResultSet +import fastdex.build.lib.snapshoot.sourceset.SourceSetSnapshoot +import fastdex.build.lib.snapshoot.string.StringNode +import fastdex.build.lib.snapshoot.string.StringSnapshoot +import com.dx168.fastdex.build.variant.FastdexVariant +import org.gradle.api.Project +import fastdex.common.utils.FileUtils + +/** + * Created by tong on 17/3/31. + */ +public class ProjectSnapshoot { + FastdexVariant fastdexVariant + SourceSetSnapshoot sourceSetSnapshoot + SourceSetSnapshoot oldSourceSetSnapshoot + SourceSetDiffResultSet diffResultSet + SourceSetDiffResultSet oldDiffResultSet + StringSnapshoot dependenciesSnapshoot + StringSnapshoot oldDependenciesSnapshoot + + ProjectSnapshoot(FastdexVariant fastdexVariant) { + this.fastdexVariant = fastdexVariant + } + + def loadSnapshoot() { + if (!fastdexVariant.hasDexCache) { + return + } + def project = fastdexVariant.project + //load old sourceSet + File sourceSetSnapshootFile = FastdexUtils.getSourceSetSnapshootFile(project,fastdexVariant.variantName) + oldSourceSetSnapshoot = SourceSetSnapshoot.load(sourceSetSnapshootFile,SourceSetSnapshoot.class) + + File dependenciesListFile = FastdexUtils.getCachedDependListFile(project,fastdexVariant.variantName) + oldDependenciesSnapshoot = StringSnapshoot.load(dependenciesListFile,StringSnapshoot.class) + + String oldProjectPath = fastdexVariant.metaInfo.projectPath + String curProjectPath = project.projectDir.absolutePath + + String oldRootProjectPath = fastdexVariant.metaInfo.rootProjectPath + String curRootProjectPath = project.rootProject.projectDir.absolutePath + boolean isRootProjectDirChanged = fastdexVariant.metaInfo.isRootProjectDirChanged(curRootProjectPath) + if (isRootProjectDirChanged) { + //已存在构建缓存的情况下,如果移动了项目目录要把缓存中的老的路径全部替换掉 + applyNewProjectDir(oldSourceSetSnapshoot,oldRootProjectPath,curRootProjectPath,curProjectPath) + if (oldSourceSetSnapshoot.lastDiffResult != null) { + oldSourceSetSnapshoot.lastDiffResult = null + } + //save + saveSourceSetSnapshoot(oldSourceSetSnapshoot) + + for (StringNode node : oldDependenciesSnapshoot.nodes) { + node.string = replacePath(node.string,oldRootProjectPath,curRootProjectPath) + } + saveDependenciesSnapshoot(oldDependenciesSnapshoot) + + fastdexVariant.metaInfo.projectPath = curProjectPath + fastdexVariant.metaInfo.rootProjectPath = curRootProjectPath + fastdexVariant.saveMetaInfo() + project.logger.error("==fastdex restore cache, project path changed old: ${oldProjectPath} now: ${curProjectPath}") + } + } + + def applyNewProjectDir(SourceSetSnapshoot sourceSnapshoot,String oldRootProjectPath,String curRootProjectPath,String curProjectPath) { + sourceSnapshoot.path = curProjectPath + for (StringNode node : sourceSnapshoot.nodes) { + node.setString(replacePath(node.getString(),oldRootProjectPath,curRootProjectPath)) + } + for (JavaDirectorySnapshoot snapshoot : sourceSnapshoot.directorySnapshootSet) { + snapshoot.path = replacePath(snapshoot.path,oldRootProjectPath,curRootProjectPath) + snapshoot.projectPath = replacePath(snapshoot.projectPath,oldRootProjectPath,curRootProjectPath) + } + } + + def replacePath(String path,String s,String s1) { + if (path.startsWith(s)) { + path = path.substring(s.length()); + path = s1 + path; + } + return path; + } + + def prepareEnv() { + def project = fastdexVariant.project + sourceSetSnapshoot = new SourceSetSnapshoot(project.projectDir,getProjectSrcDirSet(project)) + handleGeneratedSource(sourceSetSnapshoot) + handleLibraryDependencies(sourceSetSnapshoot) + + if (fastdexVariant.hasDexCache) { + diffResultSet = sourceSetSnapshoot.diff(oldSourceSetSnapshoot) + if (!fastdexVariant.firstPatchBuild) { + File diffResultSetFile = FastdexUtils.getDiffResultSetFile(project,fastdexVariant.variantName) + oldDiffResultSet = SourceSetDiffResultSet.load(diffResultSetFile,SourceSetDiffResultSet.class) + } + } + } + + /** + * 把自动生成的代码添加到源码快照中(R.java、buildConfig.java) + * @param snapshoot + */ + def handleGeneratedSource(SourceSetSnapshoot snapshoot) { + List androidLibDependencies = new ArrayList<>() + for (LibDependency libDependency : fastdexVariant.libraryDependencies) { + if (libDependency.androidLibrary) { + androidLibDependencies.add(libDependency) + } + } + + //TODO change api + //File rDir = new File(fastdexVariant.project.buildDir,"generated${File.separator}source${File.separator}r${File.separator}${fastdexVariant.androidVariant.dirName}${File.separator}") + File rDir = fastdexVariant.androidVariant.getVariantData().getScope().getRClassSourceOutputDir() + //r + JavaDirectorySnapshoot rSnapshoot = new JavaDirectorySnapshoot(rDir,getAllRjavaPath(fastdexVariant.project,androidLibDependencies)) + rSnapshoot.projectPath = fastdexVariant.project.projectDir.absolutePath + snapshoot.addJavaDirectorySnapshoot(rSnapshoot) + + //buildconfig + List projectList = new ArrayList<>() + projectList.add(fastdexVariant.project) + for (LibDependency libDependency : androidLibDependencies) { + projectList.add(libDependency.dependencyProject) + } + + String buildTypeName = fastdexVariant.androidVariant.getBuildType().buildType.getName() + String dirName = fastdexVariant.androidVariant.dirName + //buildTypeName "debug" + //dirName "debug" + //libraryVariantdirName Constants.DEFAULT_LIBRARY_VARIANT_DIR_NAME + def libraryVariantdirName = Constants.DEFAULT_LIBRARY_VARIANT_DIR_NAME + /** + * fix-issue32 https://github.com/typ0520/fastdex/issues/32 + * 正常的buildConfig目录 + * /Users/zhengmj/Desktop/TjrTaojinRoad/common/build/generated/source/buildConfig/release + * + * issue32对应的buildConfig目录 + * /Users/zhengmj/Desktop/TjrTaojinRoad/common/build/generated/source/buildConfig/taojinroad/release + */ + if (!dirName.equals(buildTypeName)) { + //buildTypeName "debug" + //dirName "xxxx/debug" + //libraryVariantdirName Constants.DEFAULT_LIBRARY_VARIANT_DIR_NAME + libraryVariantdirName = dirName.substring(0,dirName.length() - buildTypeName.length()) + libraryVariantdirName = "${libraryVariantdirName}${Constants.DEFAULT_LIBRARY_VARIANT_DIR_NAME}" + + if (libraryVariantdirName.startsWith(File.separator)) { + libraryVariantdirName = libraryVariantdirName.substring(1) + } + if (libraryVariantdirName.endsWith(File.separator)) { + libraryVariantdirName = libraryVariantdirName.substring(0,libraryVariantdirName.length() - 1) + } + } + for (int i = 0;i < projectList.size();i++) { + Project project = projectList.get(i) + String packageName = GradleUtils.getPackageName(project.android.sourceSets.main.manifest.srcFile.absolutePath) + String packageNamePath = packageName.split("\\.").join(File.separator) + //buildconfig + String buildConfigJavaRelativePath = "${packageNamePath}${File.separator}BuildConfig.java" + File buildConfigDir = null + if (i == 0) { + //TODO change api + buildConfigDir = fastdexVariant.androidVariant.getVariantData().getScope().getBuildConfigSourceOutputDir() + //buildConfigDir = new File(project.buildDir,"generated${File.separator}source${File.separator}buildConfig${File.separator}${fastdexVariant.androidVariant.dirName}${File.separator}") + } + else { + buildConfigDir = new File(project.buildDir,"generated${File.separator}source${File.separator}buildConfig${File.separator}${libraryVariantdirName}${File.separator}") + } + File buildConfigJavaFile = new File(buildConfigDir,buildConfigJavaRelativePath) + if (fastdexVariant.configuration.debug) { + fastdexVariant.project.logger.error("==fastdex buildConfigJavaFile: ${buildConfigJavaFile}") + } + JavaDirectorySnapshoot buildConfigSnapshoot = new JavaDirectorySnapshoot(buildConfigDir,buildConfigJavaFile.absolutePath) + buildConfigSnapshoot.projectPath = project.projectDir.absolutePath + snapshoot.addJavaDirectorySnapshoot(buildConfigSnapshoot) + } + } + + /** + * 往源码快照里添加依赖的工程源码路径 + * @param snapshoot + */ + def handleLibraryDependencies(SourceSetSnapshoot snapshoot) { + for (LibDependency libDependency : fastdexVariant.libraryDependencies) { + Set srcDirSet = getProjectSrcDirSet(libDependency.dependencyProject) + + for (File file : srcDirSet) { + JavaDirectorySnapshoot javaDirectorySnapshoot = new JavaDirectorySnapshoot(file) + javaDirectorySnapshoot.projectPath = libDependency.dependencyProject.projectDir.absolutePath + snapshoot.addJavaDirectorySnapshoot(javaDirectorySnapshoot) + } + } + } + + /** + * 获取application工程自身和依赖的aar工程的所有R文件相对路径 + * @param appProject + * @param androidLibDependencies + * @return + */ + def getAllRjavaPath(Project appProject,List androidLibDependencies) { + File rDir = new File(appProject.buildDir,"generated${File.separator}source${File.separator}r${File.separator}${fastdexVariant.androidVariant.dirName}${File.separator}") + List fileList = new ArrayList<>() + for (LibDependency libDependency : androidLibDependencies) { + String packageName = GradleUtils.getPackageName(libDependency.dependencyProject.android.sourceSets.main.manifest.srcFile.absolutePath) + String packageNamePath = packageName.split("\\.").join(File.separator) + + String rjavaRelativePath = "${packageNamePath}${File.separator}R.java" + File rjavaFile = new File(rDir,rjavaRelativePath) + fileList.add(rjavaFile) + + if (fastdexVariant.configuration.debug) { + fastdexVariant.project.logger.error("==fastdex rjavaFile: ${rjavaFile}") + } + } + return fileList + } + + /** + * 获取工程对应的所有源码目录 + * @param project + * @return + */ + def getProjectSrcDirSet(Project project) { + def srcDirs = null + if (project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")) { + srcDirs = project.android.sourceSets.main.java.srcDirs + } + else { + srcDirs = project.sourceSets.main.java.srcDirs + } + if (fastdexVariant.configuration.debug) { + project.logger.error("==fastdex: ${project} ${srcDirs}") + } + Set srcDirSet = new HashSet<>() + if (srcDirs != null) { + for (java.lang.Object src : srcDirs) { + if (src instanceof File) { + srcDirSet.add(src) + } + else if (src instanceof String) { + srcDirSet.add(new File(src)) + } + } + } + return srcDirSet + } + + /** + * 保存源码快照信息 + * @param snapshoot + * @return + */ + def saveSourceSetSnapshoot(SourceSetSnapshoot snapshoot) { + snapshoot.serializeTo(new FileOutputStream(FastdexUtils.getSourceSetSnapshootFile(fastdexVariant.project,fastdexVariant.variantName))) + } + + /** + * 保存当前的源码快照信息 + * @return + */ + def saveCurrentSourceSetSnapshoot() { + saveSourceSetSnapshoot(sourceSetSnapshoot) + } + + /** + * 保存源码对比结果 + * @return + */ + def saveDiffResultSet() { + if (diffResultSet != null && !diffResultSet.changedJavaFileDiffInfos.empty) { + File diffResultSetFile = FastdexUtils.getDiffResultSetFile(fastdexVariant.project,fastdexVariant.variantName) + //全量打包后首次java文件发生变化 + diffResultSet.serializeTo(new FileOutputStream(diffResultSetFile)) + } + } + + /** + * 删除源码对比结果 + * @return + */ + def deleteLastDiffResultSet() { + File diffResultSetFile = FastdexUtils.getDiffResultSetFile(fastdexVariant.project,fastdexVariant.variantName) + FileUtils.deleteFile(diffResultSetFile) + } + + /** + * 依赖列表是否发生变化 + * @return + */ + def isDependenciesChanged() { + if (dependenciesSnapshoot == null) { + dependenciesSnapshoot = new StringSnapshoot(GradleUtils.getCurrentDependList(fastdexVariant.project,fastdexVariant.androidVariant)) + } + + if (oldDependenciesSnapshoot == null) { + File dependenciesListFile = FastdexUtils.getCachedDependListFile(fastdexVariant.project,fastdexVariant.variantName) + oldDependenciesSnapshoot = StringSnapshoot.load(dependenciesListFile,StringSnapshoot.class) + } + return !dependenciesSnapshoot.diff(oldDependenciesSnapshoot).getAllChangedDiffInfos().isEmpty() + } + + /** + * 保存全量打包时的依赖列表 + */ + def saveDependenciesSnapshoot() { + if (dependenciesSnapshoot == null) { + dependenciesSnapshoot = new StringSnapshoot(GradleUtils.getCurrentDependList(fastdexVariant.project,fastdexVariant.androidVariant)) + } + saveDependenciesSnapshoot(dependenciesSnapshoot) + } + + /** + * 保存依赖列表 + * @param snapshoot + * @return + */ + def saveDependenciesSnapshoot(StringSnapshoot snapshoot) { + File dependenciesListFile = FastdexUtils.getCachedDependListFile(fastdexVariant.project,fastdexVariant.variantName) + + StringSnapshoot stringSnapshoot = new StringSnapshoot() + stringSnapshoot.nodes = snapshoot.nodes + stringSnapshoot.serializeTo(new FileOutputStream(dependenciesListFile)) + } + + def onDexGenerateSuccess(boolean nornalBuild,boolean dexMerge) { + if (nornalBuild) { + //save sourceSet + saveCurrentSourceSetSnapshoot() + //save dependencies + saveDependenciesSnapshoot() + } + else { + if (dexMerge) { + //save snapshoot and diffinfo + saveCurrentSourceSetSnapshoot() + deleteLastDiffResultSet() + } + } + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/TagManager.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/TagManager.groovy new file mode 100644 index 00000000..35e0ca96 --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/util/TagManager.groovy @@ -0,0 +1,72 @@ +package com.dx168.fastdex.build.util + +import org.gradle.api.Project; + +/** + * Created by tong on 17/3/14. + */ +public class TagManager { + final Project project + final String variantName + + TagManager(Project project, String variantName) { + this.project = project + this.variantName = variantName + } + + /** + * 是否有某个标志 + * @param tag + * @return + */ + public boolean hasTag(String tag) { + if (tag == null || tag.length() == 0) { + return false + } + File file = getTagFile(tag) + return file.exists() && file.isFile() + } + + /** + * 保存标记 + * @param tag + */ + public boolean saveTag(String tag) { + if (tag == null || tag.length() == 0) { + return + } + + if (!hasTag(tag)) { + File file = getTagFile(tag) + return file.createNewFile() + } + + return true + } + + /** + * 移除标记 + * @param tag + */ + public boolean deleteTag(String tag) { + if (tag == null || tag.length() == 0) { + return + } + + if (hasTag(tag)) { + File file = getTagFile(tag) + return file.delete() + } + return true + } + + private File getTagFile(String tag) { + return new File(getRootTagFile(),tag) + } + + private File getRootTagFile() { + File rootDir = new File(FastdexUtils.getBuildDir(project,variantName),"tags") + FileUtils.ensumeDir(rootDir) + return rootDir + } +} diff --git a/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/variant/FastdexVariant.groovy b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/variant/FastdexVariant.groovy new file mode 100755 index 00000000..181af51f --- /dev/null +++ b/fastdex-gradle/src/main/groovy/com/dx168/fastdex/build/variant/FastdexVariant.groovy @@ -0,0 +1,235 @@ +package com.dx168.fastdex.build.variant + +import com.dx168.fastdex.build.extension.FastdexExtension +import com.dx168.fastdex.build.task.FastdexInstantRunTask +import fastdex.common.utils.SerializeUtils +import com.dx168.fastdex.build.util.LibDependency +import com.dx168.fastdex.build.util.MetaInfo +import com.dx168.fastdex.build.util.ProjectSnapshoot +import com.dx168.fastdex.build.util.FastdexUtils +import fastdex.common.utils.FileUtils +import com.dx168.fastdex.build.util.GradleUtils +import com.dx168.fastdex.build.util.TagManager +import org.gradle.api.GradleException +import org.gradle.api.Project + +/** + * Created by tong on 17/3/10. + */ +public class FastdexVariant { + final Project project + final FastdexExtension configuration + final def androidVariant + final String variantName + final String manifestPath + final File rootBuildDir + final File buildDir + final ProjectSnapshoot projectSnapshoot + final TagManager tagManager + final Set libraryDependencies + String originPackageName + String mergedPackageName + boolean hasDexCache + boolean firstPatchBuild + boolean initialized + boolean hasJarMergingTask + boolean executedJarMerge + boolean executedDexTransform + MetaInfo metaInfo + FastdexInstantRunTask fastdexInstantRunTask + + FastdexVariant(Project project, Object androidVariant) { + this.project = project + this.androidVariant = androidVariant + + this.configuration = project.fastdex + this.variantName = androidVariant.name.capitalize() + this.manifestPath = androidVariant.outputs.first().processManifest.manifestOutputFile + this.rootBuildDir = FastdexUtils.getBuildDir(project) + this.buildDir = FastdexUtils.getBuildDir(project,variantName) + + projectSnapshoot = new ProjectSnapshoot(this) + tagManager = new TagManager(this.project,this.variantName) + libraryDependencies = LibDependency.resolveProjectDependency(project,androidVariant) + + if (configuration.dexMergeThreshold <= 0) { + throw new GradleException("dexMergeThreshold Must be greater than 0!!") + } + } + + /* + * 检查缓存是否过期,如果过期就删除 + * 1、查看app/build/fastdex/${variantName}/dex_cache目录下是否存在dex + * 2、检查当前的依赖列表和全两打包时的依赖是否一致(app/build/fastdex/${variantName}/dependencies-mapping.txt) + * 3、检查当前的依赖列表和全量打包时的依赖列表是否一致 + * 4、检查资源映射文件是否存在(app/build/fastdex/${variantName}/R.txt) + * 5、检查全量的代码jar包是否存在(app/build/fastdex/${variantName}/injected-combined.jar) + */ + void prepareEnv() { + if (initialized) { + return + } + initialized = true + hasDexCache = FastdexUtils.hasDexCache(project,variantName) + if (hasDexCache) { + File diffResultSetFile = FastdexUtils.getDiffResultSetFile(project,variantName) + if (!FileUtils.isLegalFile(diffResultSetFile)) { + firstPatchBuild = true + } + + try { + metaInfo = MetaInfo.load(project,variantName) + if (metaInfo == null) { + File metaInfoFile = FastdexUtils.getMetaInfoFile(project,variantName) + + if (FileUtils.isLegalFile(metaInfoFile)) { + throw new CheckException("parse json content fail: ${FastdexUtils.getMetaInfoFile(project,variantName)}") + } + else { + throw new CheckException("miss meta info file: ${FastdexUtils.getMetaInfoFile(project,variantName)}") + } + } + + File cachedDependListFile = FastdexUtils.getCachedDependListFile(project,variantName) + if (!FileUtils.isLegalFile(cachedDependListFile)) { + throw new CheckException("miss depend list file: ${cachedDependListFile}") + } + + File sourceSetSnapshootFile = FastdexUtils.getSourceSetSnapshootFile(project,variantName) + if (!FileUtils.isLegalFile(sourceSetSnapshootFile)) { + throw new CheckException("miss sourceSet snapshoot file: ${sourceSetSnapshootFile}") + } + + File resourceMappingFile = FastdexUtils.getResourceMappingFile(project,variantName) + if (!FileUtils.isLegalFile(resourceMappingFile)) { + throw new CheckException("miss resource mapping file: ${resourceMappingFile}") + } + + if (configuration.useCustomCompile) { + File injectedJarFile = FastdexUtils.getInjectedJarFile(project,variantName) + if (!FileUtils.isLegalFile(injectedJarFile)) { + throw new CheckException("miss injected jar file: ${injectedJarFile}") + } + } + + try { + projectSnapshoot.loadSnapshoot() + } catch (Throwable e) { + e.printStackTrace() + throw new CheckException(e) + } + + if (projectSnapshoot.isDependenciesChanged()) { + throw new CheckException("dependencies changed") + } + } catch (CheckException e) { + hasDexCache = false + project.logger.error("==fastdex ${e.getMessage()}") + project.logger.error("==fastdex we will remove ${variantName.toLowerCase()} cache") + } + } + + if (hasDexCache) { + project.logger.error("==fastdex discover dex cache for ${variantName.toLowerCase()}") + } + else { + metaInfo = new MetaInfo() + metaInfo.projectPath = project.projectDir.absolutePath + metaInfo.rootProjectPath = project.rootProject.projectDir.absolutePath + metaInfo.variantName = variantName + FastdexUtils.cleanCache(project,variantName) + FileUtils.ensumeDir(buildDir) + } + + projectSnapshoot.prepareEnv() + } + + /** + * 获取原始manifest文件的package节点的值 + * @return + */ + public String getOriginPackageName() { + if (originPackageName != null) { + return originPackageName + } + String path = project.android.sourceSets.main.manifest.srcFile.absolutePath + originPackageName = GradleUtils.getPackageName(path) + return originPackageName + } + + /** + * 获取合并以后的manifest文件的package节点的值 + * @return + */ + public String getMergedPackageName() { + if (mergedPackageName != null) { + return mergedPackageName + } + mergedPackageName = GradleUtils.getPackageName(manifestPath) + return mergedPackageName + } + + /** + * 当dex生成以后 + * @param nornalBuild + */ + public void onDexGenerateSuccess(boolean nornalBuild,boolean dexMerge) { + if (nornalBuild) { + saveMetaInfo() + copyRTxt() + } + else { + if (dexMerge) { + //移除idx.xml public.xml + File idsXmlFile = FastdexUtils.getIdxXmlFile(project,variantName) + File publicXmlFile = FastdexUtils.getPublicXmlFile(project,variantName) + FileUtils.deleteFile(idsXmlFile) + FileUtils.deleteFile(publicXmlFile) + + copyRTxt() + } + } + copyMetaInfo2Assets() + projectSnapshoot.onDexGenerateSuccess(nornalBuild,dexMerge) + } + + def saveMetaInfo() { + File metaInfoFile = FastdexUtils.getMetaInfoFile(project,variantName) + SerializeUtils.serializeTo(new FileOutputStream(metaInfoFile),metaInfo) + } + + def copyMetaInfo2Assets() { + File metaInfoFile = FastdexUtils.getMetaInfoFile(project,variantName) + File assetsPath = androidVariant.getVariantData().getScope().getMergeAssetsOutputDir() + FileUtils.copyFileUsingStream(metaInfoFile,new File(assetsPath,metaInfoFile.getName())) + } + + /** + * 保存资源映射文件 + */ + def copyRTxt() { + File rtxtFile = new File(androidVariant.getVariantData().getScope().getSymbolLocation(),"R.txt") + if (!FileUtils.isLegalFile(rtxtFile)) { + rtxtFile = new File(project.buildDir,"${File.separator}intermediates${File.separator}symbols${File.separator}${androidVariant.dirName}${File.separator}R.txt") + } + FileUtils.copyFileUsingStream(rtxtFile,FastdexUtils.getResourceMappingFile(project,variantName)) + } + + /** + * 补丁打包是否需要执行dex merge + * @return + */ + public boolean willExecDexMerge() { + return hasDexCache && projectSnapshoot.diffResultSet.changedJavaFileDiffInfos.size() >= configuration.dexMergeThreshold + } + + private class CheckException extends Exception { + CheckException(String var1) { + super(var1) + } + + CheckException(Throwable var1) { + super(var1) + } + } +} diff --git a/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.dx168.fastdex.properties b/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.dx168.fastdex.properties new file mode 100644 index 00000000..9f6e240c --- /dev/null +++ b/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.dx168.fastdex.properties @@ -0,0 +1 @@ +implementation-class=com.dx168.fastdex.build.FastdexPlugin \ No newline at end of file diff --git a/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.github.typ0520.fastdex.properties b/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.github.typ0520.fastdex.properties new file mode 100644 index 00000000..9f6e240c --- /dev/null +++ b/fastdex-gradle/src/main/resources/META-INF/gradle-plugins/com.github.typ0520.fastdex.properties @@ -0,0 +1 @@ +implementation-class=com.dx168.fastdex.build.FastdexPlugin \ No newline at end of file diff --git a/fastdex-gradle/src/main/resources/fastdex-dex-merge.jar b/fastdex-gradle/src/main/resources/fastdex-dex-merge.jar new file mode 100755 index 00000000..11349d14 Binary files /dev/null and b/fastdex-gradle/src/main/resources/fastdex-dex-merge.jar differ diff --git a/fastdex-gradle/src/main/resources/fastdex-runtime.dex b/fastdex-gradle/src/main/resources/fastdex-runtime.dex new file mode 100644 index 00000000..e372e9bb Binary files /dev/null and b/fastdex-gradle/src/main/resources/fastdex-runtime.dex differ diff --git a/fastdex-gradle/src/main/resources/fastdex-studio-info-macos.sh b/fastdex-gradle/src/main/resources/fastdex-studio-info-macos.sh new file mode 100755 index 00000000..56e405a7 --- /dev/null +++ b/fastdex-gradle/src/main/resources/fastdex-studio-info-macos.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +#输入gradle进程id + +#输出以下信息 +#是否是在studio上触发的构建 + +#如果是在studio上触发的构建在获取以下信息 +#android studio 是否开启了instant run +#android studio版本号 + +debug=0 + +function debug_log { + if [ $debug != 0 ];then + echo $@ + fi +} + +gradle_pid=$1 +if [ "${gradle_pid}" == "" ];then + echo "please input gradle pid" + exit -1 +fi +ps_gradle_result=$(ps -ef | grep "${gradle_pid}" | head -1) +debug_log "ps_gradle_result: ${ps_gradle_result}" + +echo $ps_gradle_result | grep "$0 ${gradle_pid}" > /dev/null +if [ $? == 0 ] || [ "${ps_gradle_result}" == "" ];then + echo "process not found id: ${gradle_pid}" + exit -1 +fi + +gradle_ppid=$(echo $ps_gradle_result | awk '{print $3}') + +ps_studio_result=$(ps -ef | grep 'MacOS/studio' | head -1) +debug_log "ps_studio_result: ${ps_studio_result}" +echo $ps_studio_result | grep 'Contents/MacOS/studio' > /dev/null + +from_studio=true +if [ $? != 0 ] || [ "${ps_studio_result}" == "" ];then + from_studio=false + + echo "from_studio=${from_studio}" + if [ "$2" != "" ];then + echo 'from_studio=false' > $2 + fi + exit 0 +fi + +studio_pid=$(echo $ps_studio_result | awk '{print $2}') +studio_home="$(echo $ps_studio_result | awk '{print $8}')" +temp_dir=$(echo $ps_studio_result | awk '{print $9}') +if [ "${temp_dir}" != "" ];then + studio_home="${studio_home} ${temp_dir}" +fi + +studio_home=${studio_home%/MacOS/studio*} +debug_log $studio_home + +info_plist="${studio_home}/Info.plist" +debug_log $info_plist +line_num=$(cat -n "${info_plist}" | grep 'CFBundleShortVersionString' | awk '{print $1}') +let line_num=$line_num+1 +debug_log $line_num +studio_version=$(sed -n "${line_num},${line_num}p" "${info_plist}") +studio_version=${studio_version#*} +studio_version=${studio_version%*} + +if [ "${gradle_ppid}" != "${studio_pid}" ];then + from_studio=false +fi + +instant_run_disabled=false +instant_run_config="${HOME}/Library/Preferences/AndroidStudio2.2/options/instant-run.xml" +if [ -f "${instant_run_config}" ];then + cat ${instant_run_config} | grep 'false' > /dev/null + if [ $? == 0 ];then + instant_run_disabled=true + fi +fi + +echo "from_studio=${from_studio}" +#echo "gradle_pid=${gradle_pid}" +#echo "gradle_ppid=${gradle_ppid}" +#echo "studio_pid=${studio_pid}" +echo "studio_home=${studio_home}" +echo "studio_version=${studio_version}" +echo "info_plist=${info_plist}" +echo "instant_run_disabled=${instant_run_disabled}" +echo "instant_run_config=${instant_run_config}" + +if [ "$2" != "" ];then + echo "from_studio=${from_studio}" > $2 + echo "gradle_pid=${gradle_pid}" >> $2 + echo "gradle_ppid=${gradle_ppid}" >> $2 + echo "studio_pid=${studio_pid}" >> $2 + echo "studio_home=${studio_home}" >> $2 + echo "studio_version=${studio_version}" >> $2 + echo "info_plist=${info_plist}" >> $2 + echo "instant_run_disabled=${instant_run_disabled}" >> $2 + echo "instant_run_config=${instant_run_config}" >> $2 +fi + +exit 0 + + diff --git a/gradle.properties b/gradle.properties index eb569fee..5c4e25d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,8 +19,8 @@ org.gradle.daemon=true -groupId=com.dx168.fastdex -version=0.1.6 +groupId=com.github.typ0520 +version=0.1.8 ANDROID_BUILD_MIN_SDK_VERSION=15 ANDROID_BUILD_TARGET_SDK_VERSION=22 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 608154d6..a1af8612 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/install.sh b/install.sh index 7d3a5178..6ea06c7f 100755 --- a/install.sh +++ b/install.sh @@ -2,4 +2,5 @@ rm -rf ~/.m2/repository/com/dx168/fastdex -sh gradlew clean install \ No newline at end of file +sh gradlew :runtime:generateRuntimeDexForRelease +sh gradlew install \ No newline at end of file diff --git a/runtime/build.gradle b/runtime/build.gradle index a5f13c08..e904e2f5 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -20,16 +20,26 @@ android { } } -//如果runtime代码有变化,生成fastdex-runtime.dex放进buildSrc/src/main/resources/fastdex-runtime.dex +dependencies { + compile project(':fastdex-common') +} + +//如果runtime代码有变化,生成fastdex-runtime.dex放进fastdex-gradle/src/main/resources/fastdex-runtime.dex project.afterEvaluate { android.libraryVariants.all { variant -> def variantName = variant.name.capitalize() + if (!"Release".equals(variantName)) { + return + } Task generateDexTask = project.tasks.create("generateRuntimeDexFor${variantName}") generateDexTask.group = 'fastdex' - generateDexTask.doFirst { + generateDexTask.dependsOn variant.javaCompile + generateDexTask.doLast { + File inputDirectory = project.file("build/intermediates/classes/release") + new File(inputDirectory,"com").deleteDir() project.copy { - from project.zipTree(project.file("build/outputs/aar/${project.name}-${variantName}.aar")) - into project.file("build/intermediates/fastdex/${variantName}") + from project.rootProject.file("fastdex-common/build/classes/main/fastdex") + into project.file("build/intermediates/classes/release/fastdex") } String sdkDirectory = project.android.getSdkDirectory() @@ -43,15 +53,23 @@ project.afterEvaluate { dxcmd = "${dxcmd}.bat" } project.file("build/outputs/fastdex/${variantName.toLowerCase()}").mkdirs() - dxcmd = "${dxcmd} --dex --output=${project.file("build/outputs/fastdex/${variantName.toLowerCase()}/fastdex-runtime.dex")} ${project.file("build/intermediates/fastdex/${variantName}/classes.jar")}" + File outDex = project.file("build/outputs/fastdex/${variantName.toLowerCase()}/fastdex-runtime.dex") + dxcmd = "${dxcmd} --dex --output=${outDex} ${inputDirectory}" + println("==dxcmd: \n${dxcmd}") def process = dxcmd.execute() int status = process.waitFor() process.destroy() if (status != 0) { throw new GradleException("generate fastdex runtime dex fail!") } + + project.copy { + from outDex + into project.rootProject.file('fastdex-gradle/src/main/resources') + } + + project.file("build/intermediates/classes/release").deleteDir() } - generateDexTask.dependsOn variant.assemble } } diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/FastdexApplication.java b/runtime/src/main/java/com/dx168/fastdex/runtime/FastdexApplication.java deleted file mode 100644 index 2973e903..00000000 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/FastdexApplication.java +++ /dev/null @@ -1,298 +0,0 @@ -package com.dx168.fastdex.runtime; - -import android.app.Application; -import android.content.ComponentCallbacks; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.util.Log; -import com.dx168.fastdex.runtime.multidex.MultiDex; -import java.lang.ref.WeakReference; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Created by tong on 17/3/11. - */ -public class FastdexApplication extends Application { - public static final String LOG_TAG = "Fastdex"; - private Application realApplication; - - private String getOriginApplicationName(Context context) { - ApplicationInfo appInfo = null; - try { - appInfo = context.getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - String msg = appInfo.metaData.getString("FASTDEX_ORIGIN_APPLICATION_CLASSNAME"); - return msg; - } - - private void createRealApplication(Context context) { - String applicationClass = getOriginApplicationName(context); - if (applicationClass != null) { - Log.d(LOG_TAG, new StringBuilder().append("About to create real application of class name = ").append(applicationClass).toString()); - - try { - Class realClass = Class.forName(applicationClass); - Constructor constructor = realClass.getConstructor(new Class[0]); - this.realApplication = ((Application) constructor.newInstance(new Object[0])); - Log.v(LOG_TAG, new StringBuilder().append("Created real app instance successfully :").append(this.realApplication).toString()); - - } catch (Exception e) { - throw new IllegalStateException(e); - } - } else { - this.realApplication = new Application(); - } - } - - protected void attachBaseContext(Context context) { - super.attachBaseContext(context); - MultiDex.install(context); - fixGoogleMultiDex(context); - createRealApplication(context); - - if (this.realApplication != null) - try { - Method attachBaseContext = ContextWrapper.class - .getDeclaredMethod("attachBaseContext", new Class[]{Context.class}); - - attachBaseContext.setAccessible(true); - attachBaseContext.invoke(this.realApplication, new Object[]{context}); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - private void fixGoogleMultiDex(Context context) { - try { - Class clazz = getClassLoader().loadClass("android.support.multidex.MultiDex"); - Field field = clazz.getDeclaredField("installedApk"); - field.setAccessible(true); - Set installedApk = (Set) field.get(null); - - installedApk.addAll(MultiDex.installedApk); - } catch (Throwable e) { - - } - } - - public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication) { - /* - The code seems to perform this: - Application realApplication = the newly instantiated (in attachBaseContext) user app - currentActivityThread = ActivityThread.currentActivityThread; - Application initialApplication = currentActivityThread.mInitialApplication; - if (initialApplication == BootstrapApplication.this) { - currentActivityThread.mInitialApplication = realApplication; - // Replace all instance of the stub application in ActivityThread#mAllApplications with the - // real one - List allApplications = currentActivityThread.mAllApplications; - for (int i = 0; i < allApplications.size(); i++) { - if (allApplications.get(i) == BootstrapApplication.this) { - allApplications.set(i, realApplication); - } - } - // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and - // ActivityThread#mResourcePackages and do two things: - // - Replace the Application instance in its mApplication field with the real one - // - Replace mResDir to point to the external resource file instead of the .apk. This is - // used as the asset path for new Resources objects. - // - Set Application#mLoadedApk to the found LoadedApk instance - ArrayMap> map1 = currentActivityThread.mPackages; - for (Map.Entry> entry : map1.entrySet()) { - Object loadedApk = entry.getValue().get(); - if (loadedApk == null) { - continue; - } - if (loadedApk.mApplication == BootstrapApplication.this) { - loadedApk.mApplication = realApplication; - if (externalResourceFile != null) { - loadedApk.mResDir = externalResourceFile; - } - realApplication.mLoadedApk = loadedApk; - } - } - // Exactly the same as above, except done for mResourcePackages instead of mPackages - ArrayMap> map2 = currentActivityThread.mResourcePackages; - for (Map.Entry> entry : map2.entrySet()) { - Object loadedApk = entry.getValue().get(); - if (loadedApk == null) { - continue; - } - if (loadedApk.mApplication == BootstrapApplication.this) { - loadedApk.mApplication = realApplication; - if (externalResourceFile != null) { - loadedApk.mResDir = externalResourceFile; - } - realApplication.mLoadedApk = loadedApk; - } - } - */ - // BootstrapApplication is created by reflection in Application#handleBindApplication() -> - // LoadedApk#makeApplication(), and its return value is used to set the Application field in all - // sorts of Android internals. - // - // Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey - // patch in the real Application instance in BootstrapApplication#onCreate(). - // - // A few places directly use the created Application instance (as opposed to the fields it is - // eventually stored in). Fortunately, it's easy to forward those to the actual real - // Application class. - try { - // Find the ActivityThread instance for the current thread - Class activityThread = Class.forName("android.app.ActivityThread"); - Object currentActivityThread = getActivityThread(context, activityThread); - // Find the mInitialApplication field of the ActivityThread to the real application - Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication"); - mInitialApplication.setAccessible(true); - Application initialApplication = (Application) mInitialApplication.get(currentActivityThread); - if (realApplication != null && initialApplication == bootstrap) { - mInitialApplication.set(currentActivityThread, realApplication); - } - // Replace all instance of the stub application in ActivityThread#mAllApplications with the - // real one - if (realApplication != null) { - Field mAllApplications = activityThread.getDeclaredField("mAllApplications"); - mAllApplications.setAccessible(true); - List allApplications = (List) mAllApplications - .get(currentActivityThread); - for (int i = 0; i < allApplications.size(); i++) { - if (allApplications.get(i) == bootstrap) { - allApplications.set(i, realApplication); - } - } - } - // Figure out how loaded APKs are stored. - // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. - Class loadedApkClass; - try { - loadedApkClass = Class.forName("android.app.LoadedApk"); - } catch (ClassNotFoundException e) { - loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); - } - Field mApplication = loadedApkClass.getDeclaredField("mApplication"); - mApplication.setAccessible(true); - Field mResDir = loadedApkClass.getDeclaredField("mResDir"); - mResDir.setAccessible(true); - // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices - // floating around. - Field mLoadedApk = null; - try { - mLoadedApk = Application.class.getDeclaredField("mLoadedApk"); - } catch (NoSuchFieldException e) { - // According to testing, it's okay to ignore this. - } - // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and - // ActivityThread#mResourcePackages and do two things: - // - Replace the Application instance in its mApplication field with the real one - // - Replace mResDir to point to the external resource file instead of the .apk. This is - // used as the asset path for new Resources objects. - // - Set Application#mLoadedApk to the found LoadedApk instance - for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) { - Field field = activityThread.getDeclaredField(fieldName); - field.setAccessible(true); - Object value = field.get(currentActivityThread); - for (Map.Entry> entry : - ((Map>) value).entrySet()) { - Object loadedApk = entry.getValue().get(); - if (loadedApk == null) { - continue; - } - if (mApplication.get(loadedApk) == bootstrap) { - if (realApplication != null) { - mApplication.set(loadedApk, realApplication); - } -// if (externalResourceFile != null) { -// mResDir.set(loadedApk, externalResourceFile); -// } - if (realApplication != null && mLoadedApk != null) { - mLoadedApk.set(realApplication, loadedApk); - } - } - } - } - } catch (Throwable e) { - throw new IllegalStateException(e); - } - } - - public static Object getActivityThread( Context context, - Class activityThread) { - try { - if (activityThread == null) { - activityThread = Class.forName("android.app.ActivityThread"); - } - Method m = activityThread.getMethod("currentActivityThread"); - m.setAccessible(true); - Object currentActivityThread = m.invoke(null); - if (currentActivityThread == null && context != null) { - // In older versions of Android (prior to frameworks/base 66a017b63461a22842) - // the currentActivityThread was built on thread locals, so we'll need to try - // even harder - Field mLoadedApk = context.getClass().getField("mLoadedApk"); - mLoadedApk.setAccessible(true); - Object apk = mLoadedApk.get(context); - Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread"); - mActivityThreadField.setAccessible(true); - currentActivityThread = mActivityThreadField.get(apk); - } - return currentActivityThread; - } catch (Throwable ignore) { - return null; - } - } - - - public Context createPackageContext(String packageName, int flags) - throws PackageManager.NameNotFoundException { - Context c = this.realApplication.createPackageContext(packageName, flags); - return c == null ? this.realApplication : c; - } - - public void registerComponentCallbacks(ComponentCallbacks callback) { - this.realApplication.registerComponentCallbacks(callback); - } - - public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { - this.realApplication.registerActivityLifecycleCallbacks(callback); - } - - public void registerOnProvideAssistDataListener(OnProvideAssistDataListener callback) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - this.realApplication.registerOnProvideAssistDataListener(callback); - } - } - - public void unregisterComponentCallbacks(ComponentCallbacks callback) { - this.realApplication.unregisterComponentCallbacks(callback); - } - - public void unregisterActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { - this.realApplication.unregisterActivityLifecycleCallbacks(callback); - } - - public void unregisterOnProvideAssistDataListener(OnProvideAssistDataListener callback) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - this.realApplication.unregisterOnProvideAssistDataListener(callback); - } - } - - public void onCreate() { - super.onCreate(); - - if (this.realApplication != null) { - monkeyPatchApplication(this,this,realApplication); - this.realApplication.onCreate(); - } - } -} - diff --git a/runtime/src/main/java/fastdex/runtime/Constants.java b/runtime/src/main/java/fastdex/runtime/Constants.java new file mode 100644 index 00000000..a7841431 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/Constants.java @@ -0,0 +1,15 @@ +package fastdex.runtime; + +import fastdex.common.ShareConstants; + +/** + * Created by tong on 17/4/28. + */ +public interface Constants extends ShareConstants { + String FASTDEX_DIR = "fastdex"; + String PATCH_DIR = "patch"; + String TEMP_DIR = "temp"; + String DEX_DIR = "dex"; + String OPT_DIR = "opt"; + String RES_DIR = "res"; +} diff --git a/runtime/src/main/java/fastdex/runtime/FastdexApplication.java b/runtime/src/main/java/fastdex/runtime/FastdexApplication.java new file mode 100644 index 00000000..f6c72222 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/FastdexApplication.java @@ -0,0 +1,191 @@ +package fastdex.runtime; + +import android.app.ActivityManager; +import android.app.Application; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Process; +import android.util.Log; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; +import fastdex.runtime.fastdex.Fastdex; +import fastdex.runtime.fd.AppInfo; +import fastdex.runtime.fd.Logging; +import fastdex.runtime.fd.MonkeyPatcher; +import fastdex.runtime.fd.Server; +import fastdex.runtime.multidex.MultiDex; + +/** + * Created by tong on 17/3/11. + */ +public class FastdexApplication extends Application { + public static final String LOG_TAG = "Fastdex"; + private Application realApplication; + + private String getOriginApplicationName(Context context) { + ApplicationInfo appInfo = null; + try { + appInfo = context.getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + String msg = appInfo.metaData.getString("FASTDEX_ORIGIN_APPLICATION_CLASSNAME"); + return msg; + } + + private void createRealApplication(Context context) { + String applicationClass = getOriginApplicationName(context); + if (applicationClass != null) { + Log.d(LOG_TAG, new StringBuilder().append("About to create real application of class name = ").append(applicationClass).toString()); + + try { + Class realClass = Class.forName(applicationClass); + Constructor constructor = realClass.getConstructor(new Class[0]); + this.realApplication = ((Application) constructor.newInstance(new Object[0])); + Log.v(LOG_TAG, new StringBuilder().append("Created real app instance successfully :").append(this.realApplication).toString()); + + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + this.realApplication = new Application(); + } + } + + protected void attachBaseContext(Context context) { + super.attachBaseContext(context); + MultiDex.install(context); + fixGoogleMultiDex(context); + Fastdex.get(context).onAttachBaseContext(this); + createRealApplication(context); + + if (this.realApplication != null) { + try { + Method attachBaseContext = ContextWrapper.class + .getDeclaredMethod("attachBaseContext", new Class[]{Context.class}); + + attachBaseContext.setAccessible(true); + attachBaseContext.invoke(this.realApplication, new Object[]{context}); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + } + + private void fixGoogleMultiDex(Context context) { + try { + Class clazz = getClassLoader().loadClass("android.support.multidex.MultiDex"); + Field field = clazz.getDeclaredField("installedApk"); + field.setAccessible(true); + Set installedApk = (Set) field.get(null); + + installedApk.addAll(MultiDex.installedApk); + } catch (Throwable e) { + + } + } + + public Context createPackageContext(String packageName, int flags) + throws PackageManager.NameNotFoundException { + Context c = this.realApplication.createPackageContext(packageName, flags); + return c == null ? this.realApplication : c; + } + + public void registerComponentCallbacks(ComponentCallbacks callback) { + this.realApplication.registerComponentCallbacks(callback); + } + + public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { + this.realApplication.registerActivityLifecycleCallbacks(callback); + } + + public void registerOnProvideAssistDataListener(OnProvideAssistDataListener callback) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.realApplication.registerOnProvideAssistDataListener(callback); + } + } + + public void unregisterComponentCallbacks(ComponentCallbacks callback) { + this.realApplication.unregisterComponentCallbacks(callback); + } + + public void unregisterActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { + this.realApplication.unregisterActivityLifecycleCallbacks(callback); + } + + public void unregisterOnProvideAssistDataListener(OnProvideAssistDataListener callback) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.realApplication.unregisterOnProvideAssistDataListener(callback); + } + } + + public void onCreate() { + super.onCreate(); + + if (Fastdex.get(this).isFastdexEnabled()) { + startServer(); + } + if (this.realApplication != null) { + MonkeyPatcher.monkeyPatchApplication(this,this,realApplication); + this.realApplication.onCreate(); + } + } + + private void startServer() { + Log.d(Logging.LOG_TAG, "Starting Instant Run Server for " + getPackageName()); + + // Start server, unless we're in a multi-process scenario and this isn't the + // primary process + try { + boolean foundPackage = false; + int pid = Process.myPid(); + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + List processes = manager.getRunningAppProcesses(); + + boolean startServer; + if (processes != null && processes.size() > 1) { + // Multiple processes: look at each, and if the process name matches + // the package name (for the current pid), it's the main process. + startServer = false; + for (ActivityManager.RunningAppProcessInfo processInfo : processes) { + if (AppInfo.applicationId.equals(processInfo.processName)) { + foundPackage = true; + if (processInfo.pid == pid) { + startServer = true; + break; + } + } + } + if (!startServer && !foundPackage) { + // Safety check: If for some reason we didn't even find the main package, + // start the server anyway. This safeguards against apps doing strange + // things with the process name. + startServer = true; + Log.d(Logging.LOG_TAG, "Multiprocess but didn't find process with package: " + + "starting server anyway"); + } + } else { + // If there is only one process, start the server. + startServer = true; + } + + if (startServer) { + Server.create(this); + } else { + Log.d(Logging.LOG_TAG, "In secondary process: Not starting server"); + + } + } catch (Throwable t) { + Log.d(Logging.LOG_TAG, "Failed during multi process check", t); + Server.create(this); + } + } +} + diff --git a/runtime/src/main/java/fastdex/runtime/FastdexRuntimeException.java b/runtime/src/main/java/fastdex/runtime/FastdexRuntimeException.java new file mode 100644 index 00000000..46c19a02 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/FastdexRuntimeException.java @@ -0,0 +1,19 @@ +package fastdex.runtime; + +/** + * Created by tong on 17/4/24. + */ +public class FastdexRuntimeException extends RuntimeException { + public FastdexRuntimeException(String detailMessage) { + super(detailMessage); + } + + public FastdexRuntimeException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public FastdexRuntimeException(Throwable throwable) { + super(throwable); + } +} + diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/antilazyload/AntilazyLoad.java b/runtime/src/main/java/fastdex/runtime/antilazyload/AntilazyLoad.java similarity index 68% rename from runtime/src/main/java/com/dx168/fastdex/runtime/antilazyload/AntilazyLoad.java rename to runtime/src/main/java/fastdex/runtime/antilazyload/AntilazyLoad.java index 447e43ec..5eb96667 100644 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/antilazyload/AntilazyLoad.java +++ b/runtime/src/main/java/fastdex/runtime/antilazyload/AntilazyLoad.java @@ -1,4 +1,4 @@ -package com.dx168.fastdex.runtime.antilazyload; +package fastdex.runtime.antilazyload; /** * Created by tong on 17/3/15. diff --git a/runtime/src/main/java/fastdex/runtime/fastdex/Fastdex.java b/runtime/src/main/java/fastdex/runtime/fastdex/Fastdex.java new file mode 100644 index 00000000..a71d6f50 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fastdex/Fastdex.java @@ -0,0 +1,177 @@ +package fastdex.runtime.fastdex; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import dalvik.system.PathClassLoader; +import fastdex.common.ShareConstants; +import fastdex.common.utils.FileUtils; +import fastdex.runtime.Constants; +import fastdex.runtime.FastdexApplication; +import fastdex.runtime.FastdexRuntimeException; +import fastdex.runtime.fd.Server; +import fastdex.runtime.loader.SystemClassLoaderAdder; +import fastdex.runtime.loader.ResourcePatcher; +import fastdex.runtime.loader.shareutil.SharePatchFileUtil; + +/** + * Created by tong on 17/4/29. + */ +public class Fastdex { + public static final String LOG_TAG = Fastdex.class.getSimpleName(); + + private static Fastdex instance; + + final RuntimeMetaInfo runtimeMetaInfo; + final File fastdexDirectory; + final File patchDirectory; + final File tempDirectory; +// final File dexDirectory; +// final File resourceDirectory; + private boolean fastdexEnabled = true; + + public static Fastdex get(Context context) { + if (instance == null) { + synchronized (Fastdex.class) { + if (instance == null) { + instance = new Fastdex(context); + } + } + } + return instance; + } + + private Context applicationContext; + + public Fastdex(Context applicationContext) { + this.applicationContext = applicationContext; + + fastdexDirectory = SharePatchFileUtil.getFastdexDirectory(applicationContext); + patchDirectory = SharePatchFileUtil.getPatchDirectory(applicationContext); + tempDirectory = SharePatchFileUtil.getPatchTempDirectory(applicationContext); +// dexDirectory = new File(fastdexDirectory,Constants.DEX_DIR); +// resourceDirectory = new File(fastdexDirectory,Constants.RES_DIR); + + RuntimeMetaInfo metaInfo = RuntimeMetaInfo.load(this); + RuntimeMetaInfo assetsMetaInfo = null; + try { + InputStream is = applicationContext.getAssets().open(ShareConstants.META_INFO_FILENAME); + String assetsMetaInfoJson = new String(FileUtils.readStream(is)); + assetsMetaInfo = RuntimeMetaInfo.load(assetsMetaInfoJson); + if (assetsMetaInfo == null) { + throw new NullPointerException("AssetsMetaInfo can not be null!!!"); + } + Log.d(Fastdex.LOG_TAG,"load meta-info from assets: \n" + assetsMetaInfoJson); + if (metaInfo == null) { + assetsMetaInfo.save(this); + metaInfo = assetsMetaInfo; + File metaInfoFile = new File(fastdexDirectory, ShareConstants.META_INFO_FILENAME); + if (!FileUtils.isLegalFile(metaInfoFile)) { + throw new FastdexRuntimeException("save meta-info fail: " + metaInfoFile.getAbsolutePath()); + } + } + else if (!metaInfo.equals(assetsMetaInfo)) { + File metaInfoFile = new File(fastdexDirectory, ShareConstants.META_INFO_FILENAME); + String metaInfoJson = new String(FileUtils.readContents(metaInfoFile)); + Log.d(Fastdex.LOG_TAG,"load meta-info from files: \n" + metaInfoJson); + Log.d(Fastdex.LOG_TAG,"meta-info content changed clean"); + + FileUtils.cleanDir(fastdexDirectory); + FileUtils.cleanDir(tempDirectory); + assetsMetaInfo.save(this); + metaInfo = assetsMetaInfo; + } + } catch (Throwable e) { + e.printStackTrace(); + fastdexEnabled = false; + Log.d(LOG_TAG,"fastdex disabled: " + e.getMessage()); + } + + this.runtimeMetaInfo = metaInfo; + } + + public Context getApplicationContext() { + return applicationContext; + } + + public void onAttachBaseContext(FastdexApplication fastdexApplication) { + if (!fastdexEnabled) { + return; + } + if (!TextUtils.isEmpty(runtimeMetaInfo.getPreparedPatchPath())) { + if (!TextUtils.isEmpty(runtimeMetaInfo.getLastPatchPath())) { + FileUtils.deleteDir(new File(runtimeMetaInfo.getLastPatchPath())); + } + File preparedPatchDir = new File(runtimeMetaInfo.getPreparedPatchPath()); + File patchDir = patchDirectory; + + FileUtils.deleteDir(patchDir); + preparedPatchDir.renameTo(patchDir); + + runtimeMetaInfo.setLastPatchPath(runtimeMetaInfo.getPatchPath()); + runtimeMetaInfo.setPreparedPatchPath(null); + runtimeMetaInfo.setPatchPath(patchDir.getAbsolutePath()); + runtimeMetaInfo.save(this); + } + + if (TextUtils.isEmpty(runtimeMetaInfo.getPatchPath())) { + return; + } + + final File dexDirectory = new File(new File(runtimeMetaInfo.getPatchPath()),Constants.DEX_DIR); + final File optDirectory = new File(new File(runtimeMetaInfo.getPatchPath()),Constants.OPT_DIR); + final File resourceDirectory = new File(new File(runtimeMetaInfo.getPatchPath()),Constants.RES_DIR); + FileUtils.ensumeDir(optDirectory); + File resourceApkFile = new File(resourceDirectory,Constants.RESOURCE_APK_FILE_NAME); + if (FileUtils.isLegalFile(resourceApkFile)) { + Log.d(LOG_TAG,"apply res patch: " + resourceApkFile); + try { + ResourcePatcher.monkeyPatchExistingResources(applicationContext,resourceApkFile.getAbsolutePath()); + } catch (Throwable throwable) { + throw new FastdexRuntimeException(throwable); + } + } + + File mergedPatchDex = new File(dexDirectory,ShareConstants.MERGED_PATCH_DEX); + File patchDex = new File(dexDirectory,ShareConstants.PATCH_DEX); + + ArrayList dexList = new ArrayList<>(); + if (FileUtils.isLegalFile(mergedPatchDex)) { + dexList.add(mergedPatchDex); + } + if (FileUtils.isLegalFile(patchDex)) { + dexList.add(patchDex); + } + + if (!dexList.isEmpty()) { + PathClassLoader classLoader = (PathClassLoader) Fastdex.class.getClassLoader(); + try { + Log.d(LOG_TAG,"apply dex patch: " + dexList); + SystemClassLoaderAdder.installDexes(fastdexApplication,classLoader,optDirectory,dexList); + } catch (Throwable throwable) { + throw new FastdexRuntimeException(throwable); + } + } + + Server.showToast("fastdex, apply patch successful",applicationContext); + } + + public File getFastdexDirectory() { + return fastdexDirectory; + } + + public File getTempDirectory() { + return tempDirectory; + } + + public RuntimeMetaInfo getRuntimeMetaInfo() { + return runtimeMetaInfo; + } + + public boolean isFastdexEnabled() { + return fastdexEnabled; + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fastdex/RuntimeMetaInfo.java b/runtime/src/main/java/fastdex/runtime/fastdex/RuntimeMetaInfo.java new file mode 100644 index 00000000..7e698737 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fastdex/RuntimeMetaInfo.java @@ -0,0 +1,129 @@ +package fastdex.runtime.fastdex; + +import android.util.Log; +import com.google.gson.Gson; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import fastdex.common.ShareConstants; +import fastdex.common.utils.FileUtils; +import fastdex.common.utils.SerializeUtils; +import fastdex.runtime.fd.Logging; + +/** + * Created by tong on 17/4/29. + */ +public class RuntimeMetaInfo { + /** + * 全量编译完成的时间 + */ + private long buildMillis; + + private String variantName; + + private String lastPatchPath; + + private String patchPath; + + private String preparedPatchPath; + + public long getBuildMillis() { + return buildMillis; + } + + public void setBuildMillis(long buildMillis) { + this.buildMillis = buildMillis; + } + + public String getVariantName() { + return variantName; + } + + public void setVariantName(String variantName) { + this.variantName = variantName; + } + + public String getLastPatchPath() { + return lastPatchPath; + } + + public void setLastPatchPath(String lastPatchPath) { + this.lastPatchPath = lastPatchPath; + } + + public String getPatchPath() { + return patchPath; + } + + public void setPatchPath(String patchPath) { + this.patchPath = patchPath; + } + + public String getPreparedPatchPath() { + return preparedPatchPath; + } + + public void setPreparedPatchPath(String preparedPatchPath) { + this.preparedPatchPath = preparedPatchPath; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RuntimeMetaInfo metaInfo = (RuntimeMetaInfo) o; + + if (buildMillis != metaInfo.buildMillis) return false; + return variantName != null ? variantName.equals(metaInfo.variantName) : metaInfo.variantName == null; + + } + + @Override + public int hashCode() { + int result = (int) (buildMillis ^ (buildMillis >>> 32)); + result = 31 * result + (variantName != null ? variantName.hashCode() : 0); + return result; + } + + public void save(Fastdex fastdex) { + File metaInfoFile = new File(fastdex.fastdexDirectory, ShareConstants.META_INFO_FILENAME); + try { + SerializeUtils.serializeTo(metaInfoFile,this); + } catch (IOException e) { + e.printStackTrace(); + Log.e(Logging.LOG_TAG,e.getMessage()); + } + } + + public static RuntimeMetaInfo load(Fastdex fastdex) { + File metaInfoFile = new File(fastdex.fastdexDirectory, ShareConstants.META_INFO_FILENAME); + try { + return new Gson().fromJson(new String(FileUtils.readContents(metaInfoFile)),RuntimeMetaInfo.class); + } catch (Throwable e) { + Log.e(Logging.LOG_TAG,e.getMessage()); + } + + return null; + } + + public static RuntimeMetaInfo load(InputStream is) { + try { + return new Gson().fromJson(new String(FileUtils.readStream(is)),RuntimeMetaInfo.class); + } catch (Throwable e) { + Log.e(Logging.LOG_TAG,e.getMessage()); + } + + return null; + } + + public static RuntimeMetaInfo load(String json) { + try { + return new Gson().fromJson(json,RuntimeMetaInfo.class); + } catch (Throwable e) { + Log.e(Logging.LOG_TAG,e.getMessage()); + } + + return null; + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/AppInfo.java b/runtime/src/main/java/fastdex/runtime/fd/AppInfo.java new file mode 100644 index 00000000..a0724228 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/AppInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fastdex.runtime.fd; + +public class AppInfo { + // Keep the structure of this class in sync with + // GenerateInstantRunAppInfoTask#writeAppInfoClass + + private AppInfo() { + } + /** + * The application id of this app (e.g. the package name). Used to pick a unique + * directory for the app's reloaded resources. (We can't look for it in the manifest, + * since we need this information very early in the app life cycle, and we don't want + * to call into the framework and cause more parts of it to be initialized before + * we've monkey-patched the application class and resource loaders.) + *

+ * (Not final: Will be replaced by byte-code manipulation at build time) + */ + @SuppressWarnings({"CanBeFinal", "StaticVariableNamingConvention"}) + public static String applicationId = null; + + /** + * A token assigned to this app at build time. This is used such that the running + * app socket server can be reasonably sure that it's responding to requests from + * the IDE. + */ + @SuppressWarnings("StaticVariableNamingConvention") + public static long token = 0L; +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/ApplicationPatch.java b/runtime/src/main/java/fastdex/runtime/fd/ApplicationPatch.java new file mode 100644 index 00000000..79a8dadf --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/ApplicationPatch.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fastdex.runtime.fd; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import fastdex.runtime.fastdex.Fastdex; +import android.util.Log; + +// This class is used in both the Android runtime and in the IDE. +// Technically we only need the write protocol on the IDE side and the +// read protocol on the Android app size, but keeping it all together and +// in sync right now. +public class ApplicationPatch { + public final String path; + public final byte[] data; + + public ApplicationPatch(String path, byte[] data) { + this.path = path; + this.data = data; + } + + @Override + public String toString() { + return "ApplicationPatch{" + + "path='" + path + '\'' + + ", data.length='" + data.length + '\'' + + '}'; + } + + // Only needed on the Android side + public static List read(DataInputStream input) throws IOException { + int changeCount = input.readInt(); + + Log.d(Fastdex.LOG_TAG, "Receiving " + changeCount + " changes"); + List changes = new ArrayList(changeCount); + for (int i = 0; i < changeCount; i++) { + String path = input.readUTF(); + int size = input.readInt(); + byte[] bytes = new byte[size]; + input.readFully(bytes); + changes.add(new ApplicationPatch(path, bytes)); + + Log.d(Fastdex.LOG_TAG, "Receiving path: " + path); + } + + return changes; + } + + + public String getPath() { + return path; + } + + + public byte[] getBytes() { + return data; + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/InstantRunService.java b/runtime/src/main/java/fastdex/runtime/fd/InstantRunService.java new file mode 100644 index 00000000..0b490e78 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/InstantRunService.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.fd; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.Process; +import android.util.Log; +import java.util.List; +/** + * Service which starts the Instant Run server; started by the IDE via + * adb shell am startservice pkg/service + */ +public class InstantRunService extends Service { + + private Server server; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + // Don't allow anyone to bind to this service. + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + Log.d(Logging.LOG_TAG, "Starting Instant Run Server for " + getPackageName()); + + // Start server, unless we're in a multi-process scenario and this isn't the + // primary process + try { + boolean foundPackage = false; + int pid = Process.myPid(); + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + List processes = manager.getRunningAppProcesses(); + + boolean startServer; + if (processes != null && processes.size() > 1) { + // Multiple processes: look at each, and if the process name matches + // the package name (for the current pid), it's the main process. + startServer = false; + for (RunningAppProcessInfo processInfo : processes) { + if (AppInfo.applicationId.equals(processInfo.processName)) { + foundPackage = true; + if (processInfo.pid == pid) { + startServer = true; + break; + } + } + } + if (!startServer && !foundPackage) { + // Safety check: If for some reason we didn't even find the main package, + // start the server anyway. This safeguards against apps doing strange + // things with the process name. + startServer = true; + Log.d(Logging.LOG_TAG, "Multiprocess but didn't find process with package: " + + "starting server anyway"); + } + } else { + // If there is only one process, start the server. + startServer = true; + } + + if (startServer) { + server = Server.create(this); + } else { + Log.d(Logging.LOG_TAG, "In secondary process: Not starting server"); + + } + } catch (Throwable t) { + Log.d(Logging.LOG_TAG, "Failed during multi process check", t); + server = Server.create(this); + } + } + + @Override + public void onDestroy() { + if (server != null) { + Log.d(Logging.LOG_TAG, "Stopping Instant Run Server for " + getPackageName()); + server.shutdown(); + } + super.onDestroy(); + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/Log.java b/runtime/src/main/java/fastdex/runtime/fd/Log.java new file mode 100644 index 00000000..31313475 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/Log.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.fd; + +import java.util.logging.Level; + +public class Log { + + public static Logging logging = null; + + public interface Logging { + void log(Level level, String string); + + boolean isLoggable(Level level); + + void log(Level level, String string, Throwable throwable); + } +} \ No newline at end of file diff --git a/runtime/src/main/java/fastdex/runtime/fd/Logging.java b/runtime/src/main/java/fastdex/runtime/fd/Logging.java new file mode 100644 index 00000000..56a1d28f --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/Logging.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.fd; + +import android.util.Log; +import java.util.logging.Level; + +/** + * Instant Run runtime logging related code + */ +public class Logging { + + /** Log tag used by instant run runtime */ + public static final String LOG_TAG = "FastdexInstantRun"; + + static { + fastdex.runtime.fd.Log.logging = + new fastdex.runtime.fd.Log.Logging() { + @Override + public void log(Level level, String string) { + log(level, string, null /* throwable */); + } + + @Override + public boolean isLoggable(Level level) { + if (level == Level.SEVERE) { + return Log.isLoggable(LOG_TAG, Log.ERROR); + } else if (level == Level.FINE) { + return Log.isLoggable(LOG_TAG, Log.VERBOSE); + } else return Log.isLoggable(LOG_TAG, Log.INFO); + } + + @Override + public void log(Level level, String string, + Throwable throwable) { + if (level == Level.SEVERE) { + if (throwable == null) { + Log.e(LOG_TAG, string); + } else { + Log.e(LOG_TAG, string, throwable); + } + } else if (level == Level.FINE) { + if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + if (throwable == null) { + Log.v(LOG_TAG, string); + } else { + Log.v(LOG_TAG, string, throwable); + } + } + } else if (Log.isLoggable(LOG_TAG, Log.INFO)) { + if (throwable == null) { + Log.i(LOG_TAG, string); + } else { + Log.i(LOG_TAG, string, throwable); + } + } + } + }; + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/MonkeyPatcher.java b/runtime/src/main/java/fastdex/runtime/fd/MonkeyPatcher.java new file mode 100644 index 00000000..1463d4ac --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/MonkeyPatcher.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fastdex.runtime.fd; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; +import static android.os.Build.VERSION_CODES.JELLY_BEAN; +import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; +import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.M; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.Build; +import android.util.ArrayMap; +import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.view.ContextThemeWrapper; + + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Code which handles live-patching resources in a running app + */ +public class MonkeyPatcher { + /** + * This utility method has nothing to do with the MonkeyPatcher per se. + * It simply calls the {@code currentActivityThread} method of {@code ActivityThread}. + */ + + public static Object getActivityThread(Context context, + Class activityThread) { + try { + if (activityThread == null) { + activityThread = Class.forName("android.app.ActivityThread"); + } + Method m = activityThread.getMethod("currentActivityThread"); + m.setAccessible(true); + Object currentActivityThread = m.invoke(null); + if (currentActivityThread == null && context != null) { + // In older versions of Android (prior to frameworks/base 66a017b63461a22842) + // the currentActivityThread was built on thread locals, so we'll need to try + // even harder + Field mLoadedApk = context.getClass().getField("mLoadedApk"); + mLoadedApk.setAccessible(true); + Object apk = mLoadedApk.get(context); + Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread"); + mActivityThreadField.setAccessible(true); + currentActivityThread = mActivityThreadField.get(apk); + } + return currentActivityThread; + } catch (Throwable ignore) { + return null; + } + } + + public static void monkeyPatchExistingResources(Context context, + String externalResourceFile, + Collection activities) { + if (externalResourceFile == null) { + return; + } + + try { + // Create a new AssetManager instance and point it to the resources installed under + // /sdcard + AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance(); + Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); + mAddAssetPath.setAccessible(true); + if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) { + throw new IllegalStateException("Could not create new AssetManager"); + } + + // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm + // in L, so we do it unconditionally. + Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); + mEnsureStringBlocks.setAccessible(true); + mEnsureStringBlocks.invoke(newAssetManager); + + if (activities != null) { + for (Activity activity : activities) { + Resources resources = activity.getResources(); + + try { + Field mAssets = Resources.class.getDeclaredField("mAssets"); + mAssets.setAccessible(true); + mAssets.set(resources, newAssetManager); + } catch (Throwable ignore) { + Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); + mResourcesImpl.setAccessible(true); + Object resourceImpl = mResourcesImpl.get(resources); + Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets"); + implAssets.setAccessible(true); + implAssets.set(resourceImpl, newAssetManager); + } + + Resources.Theme theme = activity.getTheme(); + try { + try { + Field ma = Resources.Theme.class.getDeclaredField("mAssets"); + ma.setAccessible(true); + ma.set(theme, newAssetManager); + } catch (NoSuchFieldException ignore) { + Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl"); + themeField.setAccessible(true); + Object impl = themeField.get(theme); + Field ma = impl.getClass().getDeclaredField("mAssets"); + ma.setAccessible(true); + ma.set(impl, newAssetManager); + } + + Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme"); + mt.setAccessible(true); + mt.set(activity, null); + Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme"); + mtm.setAccessible(true); + mtm.invoke(activity); + + if (SDK_INT < 24) { // As of API 24, mTheme is gone (but updates work + // without these changes + Method mCreateTheme = AssetManager.class + .getDeclaredMethod("createTheme"); + mCreateTheme.setAccessible(true); + Object internalTheme = mCreateTheme.invoke(newAssetManager); + Field mTheme = Resources.Theme.class.getDeclaredField("mTheme"); + mTheme.setAccessible(true); + mTheme.set(theme, internalTheme); + } + } catch (Throwable e) { + Log.e(Logging.LOG_TAG, "Failed to update existing theme for activity " + activity, + e); + } + + pruneResourceCaches(resources); + } + } + + // Iterate over all known Resources objects + Collection> references; + if (SDK_INT >= KITKAT) { + // Find the singleton instance of ResourcesManager + Class resourcesManagerClass = Class.forName("android.app.ResourcesManager"); + Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance"); + mGetInstance.setAccessible(true); + Object resourcesManager = mGetInstance.invoke(null); + try { + Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources"); + fMActiveResources.setAccessible(true); + @SuppressWarnings("unchecked") + ArrayMap> arrayMap = + (ArrayMap>) fMActiveResources.get(resourcesManager); + references = arrayMap.values(); + } catch (NoSuchFieldException ignore) { + Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences"); + mResourceReferences.setAccessible(true); + //noinspection unchecked + references = (Collection>) mResourceReferences.get(resourcesManager); + } + } else { + Class activityThread = Class.forName("android.app.ActivityThread"); + Field fMActiveResources = activityThread.getDeclaredField("mActiveResources"); + fMActiveResources.setAccessible(true); + Object thread = getActivityThread(context, activityThread); + @SuppressWarnings("unchecked") + HashMap> map = + (HashMap>) fMActiveResources.get(thread); + references = map.values(); + } + for (WeakReference wr : references) { + Resources resources = wr.get(); + if (resources != null) { + // Set the AssetManager of the Resources instance to our brand new one + try { + Field mAssets = Resources.class.getDeclaredField("mAssets"); + mAssets.setAccessible(true); + mAssets.set(resources, newAssetManager); + } catch (Throwable ignore) { + Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); + mResourcesImpl.setAccessible(true); + Object resourceImpl = mResourcesImpl.get(resources); + Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets"); + implAssets.setAccessible(true); + implAssets.set(resourceImpl, newAssetManager); + } + + resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); + } + } + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + private static void pruneResourceCaches(Object resources) { + // Drain TypedArray instances from the typed array pool since these can hold on + // to stale asset data + if (SDK_INT >= LOLLIPOP) { + try { + Field typedArrayPoolField = + Resources.class.getDeclaredField("mTypedArrayPool"); + typedArrayPoolField.setAccessible(true); + Object pool = typedArrayPoolField.get(resources); + Class poolClass = pool.getClass(); + Method acquireMethod = poolClass.getDeclaredMethod("acquire"); + acquireMethod.setAccessible(true); + while (true) { + Object typedArray = acquireMethod.invoke(pool); + if (typedArray == null) { + break; + } + } + } catch (Throwable ignore) { + } + } + + if (SDK_INT >= Build.VERSION_CODES.M) { + // Really should only be N; fix this as soon as it has its own API level + try { + Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); + mResourcesImpl.setAccessible(true); + // For the remainder, use the ResourcesImpl instead, where all the fields + // now live + resources = mResourcesImpl.get(resources); + } catch (Throwable ignore) { + } + } + + // Prune bitmap and color state lists etc caches + Object lock = null; + if (SDK_INT >= JELLY_BEAN_MR2) { + try { + Field field = resources.getClass().getDeclaredField("mAccessLock"); + field.setAccessible(true); + lock = field.get(resources); + } catch (Throwable ignore) { + } + } else { + try { + Field field = Resources.class.getDeclaredField("mTmpValue"); + field.setAccessible(true); + lock = field.get(resources); + } catch (Throwable ignore) { + } + } + + if (lock == null) { + lock = MonkeyPatcher.class; + } + + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lock) { + // Prune bitmap and color caches + pruneResourceCache(resources, "mDrawableCache"); + pruneResourceCache(resources,"mColorDrawableCache"); + pruneResourceCache(resources,"mColorStateListCache"); + if (SDK_INT >= M) { + pruneResourceCache(resources, "mAnimatorCache"); + pruneResourceCache(resources, "mStateListAnimatorCache"); + } else if (SDK_INT == KITKAT) { + pruneResourceCache(resources, "sPreloadedDrawables"); + pruneResourceCache(resources, "sPreloadedColorDrawables"); + pruneResourceCache(resources, "sPreloadedColorStateLists"); + } + } + } + + private static boolean pruneResourceCache(Object resources, + String fieldName) { + try { + Class resourcesClass = resources.getClass(); + Field cacheField; + try { + cacheField = resourcesClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException ignore) { + cacheField = Resources.class.getDeclaredField(fieldName); + } + cacheField.setAccessible(true); + Object cache = cacheField.get(resources); + + // Find the class which defines the onConfigurationChange method + Class type = cacheField.getType(); + if (SDK_INT < JELLY_BEAN) { + if (cache instanceof SparseArray) { + ((SparseArray) cache).clear(); + return true; + } else if (SDK_INT >= ICE_CREAM_SANDWICH && cache instanceof LongSparseArray) { + // LongSparseArray has API level 16 but was private (and available inside + // the framework) in 15 and is used for this cache. + //noinspection AndroidLintNewApi + ((LongSparseArray) cache).clear(); + return true; + } + } else if (SDK_INT < M) { + // JellyBean, KitKat, Lollipop + if ("mColorStateListCache".equals(fieldName)) { + // For some reason framework doesn't call clearDrawableCachesLocked on + // this field + if (cache instanceof LongSparseArray) { + //noinspection AndroidLintNewApi + ((LongSparseArray)cache).clear(); + } + } else if (type.isAssignableFrom(ArrayMap.class)) { + Method clearArrayMap = Resources.class.getDeclaredMethod( + "clearDrawableCachesLocked", ArrayMap.class, Integer.TYPE); + clearArrayMap.setAccessible(true); + clearArrayMap.invoke(resources, cache, -1); + return true; + } else if (type.isAssignableFrom(LongSparseArray.class)) { + try { + Method clearSparseMap = Resources.class.getDeclaredMethod( + "clearDrawableCachesLocked", LongSparseArray.class, Integer.TYPE); + clearSparseMap.setAccessible(true); + clearSparseMap.invoke(resources, cache, -1); + return true; + } catch (NoSuchMethodException e) { + if (cache instanceof LongSparseArray) { + //noinspection AndroidLintNewApi + ((LongSparseArray)cache).clear(); + return true; + } + } + } else if (type.isArray() && + type.getComponentType().isAssignableFrom(LongSparseArray.class)) { + LongSparseArray[] arrays = (LongSparseArray[])cache; + for (LongSparseArray array : arrays) { + if (array != null) { + //noinspection AndroidLintNewApi + array.clear(); + } + } + return true; + } + } else { + // Marshmallow: DrawableCache class + while (type != null) { + try { + Method configChangeMethod = type.getDeclaredMethod( + "onConfigurationChange", Integer.TYPE); + configChangeMethod.setAccessible(true); + configChangeMethod.invoke(cache, -1); + return true; + } catch (Throwable ignore) { + } + + type = type.getSuperclass(); + } + } + } catch (Throwable ignore) { + // Not logging these; while there is some checking of SDK_INT here to avoid + // doing a lot of unnecessary field lookups, it's not entirely accurate and + // errs on the side of caution (since different devices may have picked up + // different snapshots of the framework); therefore, it's normal for this + // to attempt to look up a field for a cache that isn't there; only if it's + // really there will it continue to flush that particular cache. + } + + return false; + } + + + public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication) { + /* + The code seems to perform this: + Application realApplication = the newly instantiated (in attachBaseContext) user app + currentActivityThread = ActivityThread.currentActivityThread; + Application initialApplication = currentActivityThread.mInitialApplication; + if (initialApplication == BootstrapApplication.this) { + currentActivityThread.mInitialApplication = realApplication; + // Replace all instance of the stub application in ActivityThread#mAllApplications with the + // real one + List allApplications = currentActivityThread.mAllApplications; + for (int i = 0; i < allApplications.size(); i++) { + if (allApplications.get(i) == BootstrapApplication.this) { + allApplications.set(i, realApplication); + } + } + // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and + // ActivityThread#mResourcePackages and do two things: + // - Replace the Application instance in its mApplication field with the real one + // - Replace mResDir to point to the external resource file instead of the .apk. This is + // used as the asset path for new Resources objects. + // - Set Application#mLoadedApk to the found LoadedApk instance + ArrayMap> map1 = currentActivityThread.mPackages; + for (Map.Entry> entry : map1.entrySet()) { + Object loadedApk = entry.getValue().get(); + if (loadedApk == null) { + continue; + } + if (loadedApk.mApplication == BootstrapApplication.this) { + loadedApk.mApplication = realApplication; + if (externalResourceFile != null) { + loadedApk.mResDir = externalResourceFile; + } + realApplication.mLoadedApk = loadedApk; + } + } + // Exactly the same as above, except done for mResourcePackages instead of mPackages + ArrayMap> map2 = currentActivityThread.mResourcePackages; + for (Map.Entry> entry : map2.entrySet()) { + Object loadedApk = entry.getValue().get(); + if (loadedApk == null) { + continue; + } + if (loadedApk.mApplication == BootstrapApplication.this) { + loadedApk.mApplication = realApplication; + if (externalResourceFile != null) { + loadedApk.mResDir = externalResourceFile; + } + realApplication.mLoadedApk = loadedApk; + } + } + */ + // BootstrapApplication is created by reflection in Application#handleBindApplication() -> + // LoadedApk#makeApplication(), and its return value is used to set the Application field in all + // sorts of Android internals. + // + // Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey + // patch in the real Application instance in BootstrapApplication#onCreate(). + // + // A few places directly use the created Application instance (as opposed to the fields it is + // eventually stored in). Fortunately, it's easy to forward those to the actual real + // Application class. + try { + // Find the ActivityThread instance for the current thread + Class activityThread = Class.forName("android.app.ActivityThread"); + Object currentActivityThread = getActivityThread(context, activityThread); + // Find the mInitialApplication field of the ActivityThread to the real application + Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication"); + mInitialApplication.setAccessible(true); + Application initialApplication = (Application) mInitialApplication.get(currentActivityThread); + if (realApplication != null && initialApplication == bootstrap) { + mInitialApplication.set(currentActivityThread, realApplication); + } + // Replace all instance of the stub application in ActivityThread#mAllApplications with the + // real one + if (realApplication != null) { + Field mAllApplications = activityThread.getDeclaredField("mAllApplications"); + mAllApplications.setAccessible(true); + List allApplications = (List) mAllApplications + .get(currentActivityThread); + for (int i = 0; i < allApplications.size(); i++) { + if (allApplications.get(i) == bootstrap) { + allApplications.set(i, realApplication); + } + } + } + // Figure out how loaded APKs are stored. + // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. + Class loadedApkClass; + try { + loadedApkClass = Class.forName("android.app.LoadedApk"); + } catch (ClassNotFoundException e) { + loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); + } + Field mApplication = loadedApkClass.getDeclaredField("mApplication"); + mApplication.setAccessible(true); + Field mResDir = loadedApkClass.getDeclaredField("mResDir"); + mResDir.setAccessible(true); + // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices + // floating around. + Field mLoadedApk = null; + try { + mLoadedApk = Application.class.getDeclaredField("mLoadedApk"); + } catch (NoSuchFieldException e) { + // According to testing, it's okay to ignore this. + } + // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and + // ActivityThread#mResourcePackages and do two things: + // - Replace the Application instance in its mApplication field with the real one + // - Replace mResDir to point to the external resource file instead of the .apk. This is + // used as the asset path for new Resources objects. + // - Set Application#mLoadedApk to the found LoadedApk instance + for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) { + Field field = activityThread.getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(currentActivityThread); + for (Map.Entry> entry : + ((Map>) value).entrySet()) { + Object loadedApk = entry.getValue().get(); + if (loadedApk == null) { + continue; + } + if (mApplication.get(loadedApk) == bootstrap) { + if (realApplication != null) { + mApplication.set(loadedApk, realApplication); + } +// if (externalResourceFile != null) { +// mResDir.set(loadedApk, externalResourceFile); +// } + if (realApplication != null && mLoadedApk != null) { + mLoadedApk.set(realApplication, loadedApk); + } + } + } + } + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/Paths.java b/runtime/src/main/java/fastdex/runtime/fd/Paths.java new file mode 100644 index 00000000..6d518f35 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/Paths.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.fd; + +import java.io.File; + +/** + * Shared path-related logic between Android Studio and the Instant Run server. + */ +public final class Paths { + /** Temp directory on the device */ + public static final String DEVICE_TEMP_DIR = "/data/local/tmp"; + + /** The name of the build timestamp file on the device in the data folder */ + public static final String BUILD_ID_TXT = "build-id.txt"; + + /** Name of file to write resource data into, if not extracting resources */ + public static final String RESOURCE_FILE_NAME = "resources.ap_"; + + /** Name for reload dex files */ + public static final String RELOAD_DEX_FILE_NAME = "classes.dex.3"; + + public static String getMainApkDataDirectory(String applicationId) { + return "/data/data/" + applicationId; + } + + public static String getDataDirectory(String applicationId) { + return "/data/data/" + applicationId + "/files/fastdex-instant-run"; + } + + public static String getDeviceIdFolder(String pkg) { + return DEVICE_TEMP_DIR + "/" + pkg + "-" + BUILD_ID_TXT; + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/Restarter.java b/runtime/src/main/java/fastdex/runtime/fd/Restarter.java new file mode 100644 index 00000000..b45bc0ba --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/Restarter.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fastdex.runtime.fd; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.os.Build; +import android.util.ArrayMap; +import android.util.Log; +import android.widget.Toast; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Handler capable of restarting parts of the application in order for changes to become + * apparent to the user: + *

    + *
  • Apply a tiny change immediately - possible if we can detect that the change + * is only used in a limited context (such as in a layout) and we can directly + * poke the view hierarchy and schedule a paint. + *
  • Apply a change to the current activity. We can restart just the activity + * while the app continues running. + *
  • Restart the app with state persistence (simulates what happens when a user + * puts an app in the background, then it gets killed by the memory monitor, + * and then restored when the user brings it back + *
  • Restart the app completely. + *
+ */ +public class Restarter { + /** Restart an activity. Should preserve as much state as possible. */ + public static void restartActivityOnUiThread(final Activity activity) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Resources updated: notify activities"); + } + updateActivity(activity); + } + }); + } + + private static void restartActivity(Activity activity) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "About to restart " + activity.getClass().getSimpleName()); + } + + // You can't restart activities that have parents: find the top-most activity + while (activity.getParent() != null) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, activity.getClass().getSimpleName() + + " is not a top level activity; restarting " + + activity.getParent().getClass().getSimpleName() + " instead"); + } + activity = activity.getParent(); + } + + // Directly supported by the framework! + activity.recreate(); + } + + /** + * Attempt to restart the app. Ideally this should also try to preserve as much state as + * possible: + *
    + *
  • The current activity
  • + *
  • If possible, state in the current activity, and
  • + *
  • The activity stack
  • + *
+ * + * This may require some framework support. Apparently it may already be possible + * (Dianne says to put the app in the background, kill it then restart it; need to + * figure out how to do this.) + */ + public static void restartApp(Context appContext, + Collection knownActivities, + boolean toast) { + if (!knownActivities.isEmpty()) { + // Can't live patch resources; instead, try to restart the current activity + Activity foreground = getForegroundActivity(appContext); + + if (foreground != null) { + // http://stackoverflow.com/questions/6609414/howto-programatically-restart-android-app + //noinspection UnnecessaryLocalVariable + if (toast) { + showToast(foreground, "Restarting app to apply incompatible changes"); + } + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "RESTARTING APP"); + } + @SuppressWarnings("UnnecessaryLocalVariable") // fore code clarify + Context context = foreground; + Intent intent = new Intent(context, foreground.getClass()); + int intentId = 0; + PendingIntent pendingIntent = PendingIntent.getActivity(context, intentId, + intent, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Scheduling activity " + foreground + + " to start after exiting process"); + } + } else { + showToast(knownActivities.iterator().next(), "Unable to restart app"); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Couldn't find any foreground activities to restart " + + "for resource refresh"); + } + } + System.exit(0); + } + } + + static void showToast(final Activity activity, final String text) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "About to show toast for activity " + activity + ": " + text); + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + Context context = activity.getApplicationContext(); + if (context instanceof ContextWrapper) { + Context base = ((ContextWrapper) context).getBaseContext(); + if (base == null) { + if (Log.isLoggable(Logging.LOG_TAG, Log.WARN)) { + Log.w(Logging.LOG_TAG, "Couldn't show toast: no base context"); + } + return; + } + } + + // For longer messages, leave the message up longer + int duration = Toast.LENGTH_SHORT; + if (text.length() >= 60 || text.indexOf('\n') != -1) { + duration = Toast.LENGTH_LONG; + } + + // Avoid crashing when not available, e.g. + // java.lang.RuntimeException: Can't create handler inside thread that has + // not called Looper.prepare() + Toast.makeText(activity, text, duration).show(); + } catch (Throwable e) { + if (Log.isLoggable(Logging.LOG_TAG, Log.WARN)) { + Log.w(Logging.LOG_TAG, "Couldn't show toast", e); + } + } + } + }); + } + + public static Activity getForegroundActivity(Context context) { + List list = getActivities(context, true); + return list.isEmpty() ? null : list.get(0); + } + + // http://stackoverflow.com/questions/11411395/how-to-get-current-foreground-activity-context-in-android + + public static List getActivities(Context context, boolean foregroundOnly) { + List list = new ArrayList(); + try { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Object activityThread = MonkeyPatcher.getActivityThread(context, activityThreadClass); + Field activitiesField = activityThreadClass.getDeclaredField("mActivities"); + activitiesField.setAccessible(true); + + Collection c; + Object collection = activitiesField.get(activityThread); + + if (collection instanceof HashMap) { + // Older platforms + Map activities = (HashMap) collection; + c = activities.values(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && + collection instanceof ArrayMap) { + ArrayMap activities = (ArrayMap) collection; + c = activities.values(); + } else { + return list; + } + for (Object activityRecord : c) { + Class activityRecordClass = activityRecord.getClass(); + if (foregroundOnly) { + Field pausedField = activityRecordClass.getDeclaredField("paused"); + pausedField.setAccessible(true); + if (pausedField.getBoolean(activityRecord)) { + continue; + } + } + Field activityField = activityRecordClass.getDeclaredField("activity"); + activityField.setAccessible(true); + Activity activity = (Activity) activityField.get(activityRecord); + if (activity != null) { + list.add(activity); + } + } + } catch (Throwable ignore) { + } + return list; + } + + private static void updateActivity(Activity activity) { + // This method can be called for activities that are not in the foreground, as long + // as some of its resources have been updated. Therefore we'll need to make sure + // that this activity is in the foreground, and if not do nothing. Ways to do + // that are outlined here: + // http://stackoverflow.com/questions/3667022/checking-if-an-android-application-is-running-in-the-background/5862048#5862048 + + // Try to force re-layout; there are many approaches; see + // http://stackoverflow.com/questions/5991968/how-to-force-an-entire-layout-view-refresh + + // This doesn't seem to update themes properly -- may need to do recreate() instead! + //getWindow().getDecorView().findViewById(android.R.id.content).invalidate(); + + // This is a bit of a sledgehammer. We should consider having an incremental updater, + // similar to IntelliJ's Look & Feel updater which iterates to the view hierarchy + // and tries to incrementally refresh the LAF delegates and force a repaint. + // On the other hand, we may never be able to succeed with that, since there could be + // UI elements on the screen cached from callbacks. I should probably *not* attempt + // to try to poke the user's data models; recreating the current layout should be + // enough (e.g. if a layout references @string/foo, we'll recreate those widgets + // if (mLastContentView != -1) { + // setContentView(mLastContentView); + // } else { + // recreate(); + // } + // -- nope, even that's iffy. I had code which *after* calling setContentView would + // do some findViewById calls etc to reinitialize views. + // + // So what I should really try to do is have some knowledge about what changed, + // and see if I can figure out that the change is minor (e.g. doesn't affect themes + // or layout parameters etc), and if so, just try to poke the view hierarchy directly, + // and if not, just recreate + + // if (changeManager.isSimpleDelta()) { + // changeManager.applyDirectly(this); + // } else { + + + // Note: This doesn't handle manifest changes like changing the application title + + restartActivity(activity); + } +} diff --git a/runtime/src/main/java/fastdex/runtime/fd/Server.java b/runtime/src/main/java/fastdex/runtime/fd/Server.java new file mode 100644 index 00000000..dad3b3f6 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/fd/Server.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fastdex.runtime.fd; + +import static fastdex.common.fd.ProtocolConstants.MESSAGE_EOF; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_PATCHES; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_PATH_CHECKSUM; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_PATH_EXISTS; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_PING; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_RESTART_ACTIVITY; +import static fastdex.common.fd.ProtocolConstants.MESSAGE_SHOW_TOAST; +import static fastdex.common.fd.ProtocolConstants.PROTOCOL_IDENTIFIER; +import static fastdex.common.fd.ProtocolConstants.PROTOCOL_VERSION; +import static fastdex.common.fd.ProtocolConstants.UPDATE_MODE_COLD_SWAP; +import static fastdex.common.fd.ProtocolConstants.UPDATE_MODE_HOT_SWAP; +import static fastdex.common.fd.ProtocolConstants.UPDATE_MODE_NONE; +import static fastdex.common.fd.ProtocolConstants.UPDATE_MODE_WARM_SWAP; + +import android.app.Activity; +import android.content.Context; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.os.Handler; +import android.util.Log; +import fastdex.common.ShareConstants; +import fastdex.common.utils.FileUtils; +import fastdex.runtime.Constants; +import fastdex.runtime.fastdex.Fastdex; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +/** + * Server running in the app listening for messages from the IDE and updating the code and resources + * when provided + */ +public class Server { + + /** + * Temporary debugging: have the server emit a message to the log every 30 seconds to + * indicate whether it's still alive + */ + private static final boolean POST_ALIVE_STATUS = false; + + private LocalServerSocket serverSocket; + + private final Context context; + + private final Handler handler = new Handler(); + + private static int wrongTokenCount; + + + public static Server create(Context context) { + return new Server(context.getPackageName(), context); + } + + private Server(String packageName, Context context) { + this.context = context; + try { + serverSocket = new LocalServerSocket(packageName); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Starting server socket listening for package " + packageName + + " on " + serverSocket.getLocalSocketAddress()); + } + } catch (IOException e) { + Log.e(Logging.LOG_TAG, "IO Error creating local socket at " + packageName, e); + return; + } + startServer(); + + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Started server for package " + packageName); + } + } + + private void startServer() { + try { + Thread socketServerThread = new Thread(new SocketServerThread()); + socketServerThread.start(); + } catch (Throwable e) { + // Make sure an exception doesn't cause the rest of the user's + // onCreate() method to be invoked + if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) { + Log.e(Logging.LOG_TAG, "Fatal error starting Instant Run server", e); + } + } + } + + public void shutdown() { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException ignore) { + } + serverSocket = null; + } + } + + private class SocketServerThread extends Thread { + @Override + public void run() { + if (POST_ALIVE_STATUS) { + final Handler handler = new Handler(); + Timer timer = new Timer(); + TimerTask task = new TimerTask() { + @Override + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + Log.v(Logging.LOG_TAG, "Instant Run server still here..."); + } + }); + } + }; + + timer.schedule(task, 1, 30000L); + } + + while (true) { + try { + LocalServerSocket serverSocket = Server.this.serverSocket; + if (serverSocket == null) { + break; // stopped? + } + LocalSocket socket = serverSocket.accept(); + + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Received connection from IDE: spawning connection thread"); + } + + SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(socket); + socketServerReplyThread.run(); + + if (wrongTokenCount > 50) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Stopping server: too many wrong token connections"); + } + Server.this.serverSocket.close(); + break; + } + } catch (Throwable e) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Fatal error accepting connection on local socket", e); + } + } + } + } + } + + private class SocketServerReplyThread extends Thread { + + private final LocalSocket socket; + + SocketServerReplyThread(LocalSocket socket) { + this.socket = socket; + } + + @Override + public void run() { + try { + DataInputStream input = new DataInputStream(socket.getInputStream()); + DataOutputStream output = new DataOutputStream(socket.getOutputStream()); + try { + handle(input, output); + } finally { + try { + input.close(); + } catch (IOException ignore) { + } + try { + output.close(); + } catch (IOException ignore) { + } + } + } catch (IOException e) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Fatal error receiving messages", e); + } + } + } + + private void handle(DataInputStream input, DataOutputStream output) throws IOException { + long magic = input.readLong(); + if (magic != PROTOCOL_IDENTIFIER) { + Log.w(Logging.LOG_TAG, "Unrecognized header format " + + Long.toHexString(magic)); + return; + } + int version = input.readInt(); + + // Send current protocol version to the IDE so it can decide what to do + output.writeInt(PROTOCOL_VERSION); + + if (version != PROTOCOL_VERSION) { + Log.w(Logging.LOG_TAG, "Mismatched protocol versions; app is " + + "using version " + PROTOCOL_VERSION + " and tool is using version " + + version); + return; + } + + Fastdex fastdex = Fastdex.get(context); + + while (true) { + int message = input.readInt(); + switch (message) { + case MESSAGE_EOF: { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Received EOF from the IDE"); + } + return; + } + + case MESSAGE_PING: { + // Send an "ack" back to the IDE. + // The value of the boolean is true only when the app is in the + // foreground. + boolean active = Restarter.getForegroundActivity(context) != null; + output.writeBoolean(active); + + long buildMillis = fastdex.getRuntimeMetaInfo().getBuildMillis(); + output.writeLong(buildMillis); + String variantName = fastdex.getRuntimeMetaInfo().getVariantName(); + output.writeUTF(variantName); + output.writeInt(android.os.Process.myPid()); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Received Ping message from the IDE; " + "returned active = " + active); + } + continue; + } + + case MESSAGE_PATCHES: { + if (!authenticate(input)) { + return; + } + + try { + showToast("fastdex, receiving patch info...",context); + } catch (Throwable e) { + + } + List changes = ApplicationPatch.read(input); + if (changes == null) { + continue; + } + + boolean hasDex = hasDex(changes); + boolean hasResources = hasResources(changes); + int updateMode = input.readInt(); + updateMode = handlePatches(changes, hasResources, updateMode); + + boolean showToast = input.readBoolean(); + + // Send an "ack" back to the IDE; this is used for timing purposes only + output.writeBoolean(true); + + restart(updateMode,hasDex, hasResources, showToast); + continue; + } + + case MESSAGE_PATH_EXISTS: { + + continue; + } + + case MESSAGE_PATH_CHECKSUM: { + + continue; + } + + case MESSAGE_RESTART_ACTIVITY: { + if (!authenticate(input)) { + return; + } + + Activity activity = Restarter.getForegroundActivity(context); + if (activity != null) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Restarting activity per user request"); + } + Restarter.restartActivityOnUiThread(activity); + } + continue; + } + + case MESSAGE_SHOW_TOAST: { + String text = input.readUTF(); + showToast(text,context); + continue; + } + + default: { + if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) { + Log.e(Logging.LOG_TAG, "Unexpected message type: " + message); + } + // If we hit unexpected message types we can't really continue + // the conversation: we can misinterpret data for the unexpected + // command as separate messages with different meanings than intended + return; + } + } + } + } + + private boolean authenticate(DataInputStream input) throws IOException { + long token = input.readLong(); + if (token != AppInfo.token) { + Log.w(Logging.LOG_TAG, "Mismatched identity token from client; received " + token + + " and expected " + AppInfo.token); + wrongTokenCount++; + return false; + } + return true; + } + } + + public static void showToast(String text,Context context) { + Activity foreground = Restarter.getForegroundActivity(context); + if (foreground != null) { + Restarter.showToast(foreground, text); + } else if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Couldn't show toast (no activity) : " + text); + } + } + + private static boolean isResourcePath(String path) { + return path.equals(Constants.RESOURCE_APK_FILE_NAME) || path.equals(Paths.RESOURCE_FILE_NAME) || path.startsWith("res/"); + } + + private static boolean hasResources(List changes) { + // Any non-code patch is a resource patch (normally resources.ap_ but could + // also be individual resource files such as res/layout/activity_main.xml) + for (ApplicationPatch change : changes) { + String path = change.getPath(); + if (isResourcePath(path)) { + return true; + } + + } + return false; + } + + private static boolean isDexPath(String path) { + return path.endsWith(Constants.DEX_SUFFIX); + } + + private static boolean hasDex(List changes) { + // Any non-code patch is a resource patch (normally resources.ap_ but could + // also be individual resource files such as res/layout/activity_main.xml) + for (ApplicationPatch change : changes) { + String path = change.getPath(); + if (isDexPath(path)) { + return true; + } + + } + return false; + } + + private int handlePatches(List changes, boolean hasResources, + int updateMode) { +// if (hasResources) { +// FileManager.startUpdate(); +// } +// +// for (ApplicationPatch change : changes) { +// String path = change.getPath(); +// if (path.equals(Paths.RELOAD_DEX_FILE_NAME)) { +// updateMode = handleHotSwapPatch(updateMode, change); +// } else if (isResourcePath(path)) { +// updateMode = handleResourcePatch(updateMode, change, path); +// } +// } +// +// if (hasResources) { +// FileManager.finishUpdate(true); +// } +// +// return updateMode; + + + Fastdex fastdex = Fastdex.get(context); + File workDir = new File(fastdex.getTempDirectory(),System.currentTimeMillis() + "-" + UUID.randomUUID().toString()); + try { + for (ApplicationPatch change : changes) { + String path = change.getPath(); + if (path.endsWith(Constants.DEX_SUFFIX)) { + updateMode = handleHotSwapPatch(updateMode, change,workDir); + } else if (isResourcePath(path)) { + updateMode = handleResourcePatch(updateMode, change, path,workDir); + } + } + + fastdex.getRuntimeMetaInfo().setPreparedPatchPath(workDir.getAbsolutePath()); + fastdex.getRuntimeMetaInfo().save(fastdex); + } catch (Throwable e) { + return UPDATE_MODE_NONE; + } + return updateMode; + } + + private static int handleResourcePatch(int updateMode, ApplicationPatch patch, String path, File workDir) throws IOException { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Received resource changes (" + path + ")"); + } + + FileUtils.write2file(patch.getBytes(),new File(workDir, Constants.RES_DIR + "/" + Constants.RESOURCE_APK_FILE_NAME)); + //noinspection ResourceType + updateMode = Math.max(updateMode, UPDATE_MODE_WARM_SWAP); + return updateMode; + } + + private int handleHotSwapPatch(int updateMode, ApplicationPatch patch, File workDir) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Received incremental code patch"); + } + try { + File tempDexDir = new File(workDir, Constants.DEX_DIR + "/"); + FileUtils.write2file(patch.getBytes(),new File(tempDexDir,patch.getPath())); + } catch (Throwable e) { + Log.e(Logging.LOG_TAG, "Couldn't apply code changes", e); + updateMode = UPDATE_MODE_COLD_SWAP; + } + return UPDATE_MODE_COLD_SWAP; + } + + private void restart(int updateMode, boolean hasDex, boolean incrementalResources, boolean toast) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Finished loading changes; update mode =" + updateMode); + } + + if (updateMode == UPDATE_MODE_NONE || updateMode == UPDATE_MODE_HOT_SWAP) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Applying incremental code without restart"); + } + + if (toast) { + Activity foreground = Restarter.getForegroundActivity(context); + if (foreground != null) { + Restarter.showToast(foreground, "Applied code changes without activity restart"); + } else if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Couldn't show toast: no activity found"); + } + } + return; + } + + // android.os.Process.killProcess(android.os.Process.myPid()); + List activities = Restarter.getActivities(context, false); + + if (!hasDex && incrementalResources && updateMode == UPDATE_MODE_WARM_SWAP) { + // Try to just replace the resources on the fly! + + File resDir = new File(Fastdex.get(context).getRuntimeMetaInfo().getPreparedPatchPath(),Constants.RES_DIR); + File file = new File(resDir, ShareConstants.RESOURCE_APK_FILE_NAME); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "About to update resource file=" + file + + ", activities=" + activities); + } + + if (file != null) { + String resources = file.getPath(); + MonkeyPatcher.monkeyPatchExistingResources(context, resources, activities); + } else { + Log.e(Logging.LOG_TAG, "No resource file found to apply"); + updateMode = UPDATE_MODE_COLD_SWAP; + } + } + else { + handler.post(new Runnable() { + @Override + public void run() { + for (Activity activity : activities) { + try { + activity.finish(); + } catch (Throwable e) { + + } + } + + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + } + }); + } + + Activity activity = Restarter.getForegroundActivity(context); + if (updateMode == UPDATE_MODE_WARM_SWAP) { + if (activity != null) { + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Restarting activity only!"); + } + + boolean handledRestart = false; + try { + // Allow methods to handle their own restart by implementing + // public boolean onHandleCodeChange(long flags) { .... } + // and returning true if the change was handled manually + Method method = activity.getClass().getMethod("onHandleCodeChange", Long.TYPE); + Object result = method.invoke(activity, 0L); + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Activity " + activity + + " provided manual restart method; return " + result); + } + if (Boolean.TRUE.equals(result)) { + handledRestart = true; + if (toast) { + Restarter.showToast(activity, "Applied changes"); + } + } + } catch (Throwable ignore) { + } + + if (!handledRestart) { + if (toast) { + Restarter.showToast(activity, "Applied changes, restarted activity"); + } + Restarter.restartActivityOnUiThread(activity); + } + return; + } + + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "No activity found, falling through to do a full app restart"); + } + updateMode = UPDATE_MODE_COLD_SWAP; + } + + if (updateMode != UPDATE_MODE_COLD_SWAP) { + if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) { + Log.e(Logging.LOG_TAG, "Unexpected update mode: " + updateMode); + } + return; + } + + if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) { + Log.v(Logging.LOG_TAG, "Waiting for app to be killed and restarted by the IDE..."); + } + } +} diff --git a/runtime/src/main/java/fastdex/runtime/loader/AndroidNClassLoader.java b/runtime/src/main/java/fastdex/runtime/loader/AndroidNClassLoader.java new file mode 100644 index 00000000..39a459a0 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/loader/AndroidNClassLoader.java @@ -0,0 +1,184 @@ +/* + * Tencent is pleased to support the open source community by making Tinker available. + * + * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.loader; + +import android.annotation.TargetApi; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import dalvik.system.DexFile; +import dalvik.system.PathClassLoader; +import fastdex.runtime.loader.shareutil.ShareReflectUtil; + +/** + * Created by zhangshaowen on 16/7/24. + */ +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class AndroidNClassLoader extends PathClassLoader { + private static final String TAG = "Tinker.NClassLoader"; + + private static final String CHECK_CLASSLOADER_CLASS = "com.tencent.tinker.loader.TinkerTestAndroidNClassLoader"; + + private static ArrayList oldDexFiles = new ArrayList<>(); + private final PathClassLoader originClassLoader; + private String applicationClassName; + + private AndroidNClassLoader(String dexPath, PathClassLoader parent, Application application) { + super(dexPath, parent.getParent()); + originClassLoader = parent; + String name = application.getClass().getName(); + if (name != null && !name.equals("android.app.Application")) { + applicationClassName = name; + } + } + + private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader original, Application application) throws Exception { + //let all element "" + AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("", original, application); + Field originPathList = ShareReflectUtil.findField(original, "pathList"); + Object originPathListObject = originPathList.get(original); + //should reflect definingContext also + Field originClassloader = ShareReflectUtil.findField(originPathListObject, "definingContext"); + originClassloader.set(originPathListObject, androidNClassLoader); + //copy pathList + Field pathListField = ShareReflectUtil.findField(androidNClassLoader, "pathList"); + //just use PathClassloader's pathList + pathListField.set(androidNClassLoader, originPathListObject); + + //we must recreate dexFile due to dexCache + List additionalClassPathEntries = new ArrayList<>(); + Field dexElement = ShareReflectUtil.findField(originPathListObject, "dexElements"); + Object[] originDexElements = (Object[]) dexElement.get(originPathListObject); + for (Object element : originDexElements) { + DexFile dexFile = (DexFile) ShareReflectUtil.findField(element, "dexFile").get(element); + if (dexFile == null) { + continue; + } + additionalClassPathEntries.add(new File(dexFile.getName())); + //protect for java.lang.AssertionError: Failed to close dex file in finalizer. + oldDexFiles.add(dexFile); + } + Method makePathElements = ShareReflectUtil.findMethod(originPathListObject, "makePathElements", List.class, File.class, + List.class); + ArrayList suppressedExceptions = new ArrayList<>(); + Object[] newDexElements = (Object[]) makePathElements.invoke(originPathListObject, additionalClassPathEntries, null, suppressedExceptions); + dexElement.set(originPathListObject, newDexElements); + + try { + Class.forName(CHECK_CLASSLOADER_CLASS, true, androidNClassLoader); + } catch (Throwable thr) { + Log.e(TAG, "load TinkerTestAndroidNClassLoader fail, try to fixDexElementsForProtectedApp"); + fixDexElementsForProtectedApp(application, newDexElements); + } + + return androidNClassLoader; + } + + private static void reflectPackageInfoClassloader(Application application, ClassLoader reflectClassLoader) throws Exception { + String defBase = "mBase"; + String defPackageInfo = "mPackageInfo"; + String defClassLoader = "mClassLoader"; + + Context baseContext = (Context) ShareReflectUtil.findField(application, defBase).get(application); + Object basePackageInfo = ShareReflectUtil.findField(baseContext, defPackageInfo).get(baseContext); + Field classLoaderField = ShareReflectUtil.findField(basePackageInfo, defClassLoader); + Thread.currentThread().setContextClassLoader(reflectClassLoader); + classLoaderField.set(basePackageInfo, reflectClassLoader); + } + + public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception { + AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application); + reflectPackageInfoClassloader(application, classLoader); + return classLoader; + } + + // Basically this method would use base.apk to create a dummy DexFile object, + // then set its fileName, cookie, internalCookie field to the value + // comes from original DexFile object so that the encrypted dex would be taking effect. + private static void fixDexElementsForProtectedApp(Application application, Object[] newDexElements) throws Exception { + Field zipField = null; + Field dexFileField = null; + final Field mFileNameField = ShareReflectUtil.findField(DexFile.class, "mFileName"); + final Field mCookieField = ShareReflectUtil.findField(DexFile.class, "mCookie"); + final Field mInternalCookieField = ShareReflectUtil.findField(DexFile.class, "mInternalCookie"); + + // Always ignore the last element since it should always be the base.apk. + for (int i = 0; i < newDexElements.length - 1; ++i) { + final Object newElement = newDexElements[i]; + + if (zipField == null && dexFileField == null) { + zipField = ShareReflectUtil.findField(newElement, "zip"); + dexFileField = ShareReflectUtil.findField(newElement, "dexFile"); + } + + final DexFile origDexFile = oldDexFiles.get(i); + final String origFileName = (String) mFileNameField.get(origDexFile); + final Object origCookie = mCookieField.get(origDexFile); + final Object origInternalCookie = mInternalCookieField.get(origDexFile); + + final DexFile dupOrigDexFile = DexFile.loadDex(application.getApplicationInfo().sourceDir, null, 0); + mFileNameField.set(dupOrigDexFile, origFileName); + mCookieField.set(dupOrigDexFile, origCookie); + mInternalCookieField.set(dupOrigDexFile, origInternalCookie); + + dexFileField.set(newElement, dupOrigDexFile); + + // Just for better looking when dump new classloader. + // Avoid such output like this: DexPathList{zip file: /xx/yy/zz/uu.odex} + final File newZip = (File) zipField.get(newElement); + final String newZipPath = (newZip != null ? newZip.getAbsolutePath() : null); + if (newZipPath != null && !newZipPath.endsWith(".zip") && !newZipPath.endsWith(".jar") && !newZipPath.endsWith(".apk")) { + zipField.set(newElement, null); + } + } + } + +// public static String getLdLibraryPath(ClassLoader loader) throws Exception { +// String nativeLibraryPath; +// +// nativeLibraryPath = (String) loader.getClass() +// .getMethod("getLdLibraryPath", new Class[0]) +// .invoke(loader, new Object[0]); +// +// return nativeLibraryPath; +// } + + public Class findClass(String name) throws ClassNotFoundException { + // loader class use default pathClassloader to load + if ((name != null + && name.startsWith("com.tencent.tinker.loader.") + && !name.equals(SystemClassLoaderAdder.CHECK_DEX_CLASS) + && !name.equals(CHECK_CLASSLOADER_CLASS)) + || (applicationClassName != null && TextUtils.equals(applicationClassName, name))) { + return originClassLoader.loadClass(name); + } + return super.findClass(name); + } + + @Override + public String findLibrary(String name) { + return super.findLibrary(name); + } +} diff --git a/runtime/src/main/java/fastdex/runtime/loader/ResourcePatcher.java b/runtime/src/main/java/fastdex/runtime/loader/ResourcePatcher.java new file mode 100644 index 00000000..37f64f2b --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/loader/ResourcePatcher.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.loader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.util.ArrayMap; +import android.util.Log; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import fastdex.runtime.loader.shareutil.ShareReflectUtil; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.KITKAT; + +/** + * Created by zhangshaowen on 16/9/21. + * Thanks for Android Fragmentation + */ +public class ResourcePatcher { + private static final String TAG = "Tinker.ResourcePatcher"; + + // original object + private static Collection> references = null; + private static Object currentActivityThread = null; + private static AssetManager newAssetManager = null; + + // method + private static Method addAssetPathMethod = null; + private static Method ensureStringBlocksMethod = null; + + // field + private static Field assetsFiled = null; + private static Field resourcesImplFiled = null; + private static Field resDir = null; + private static Field packagesFiled = null; + private static Field resourcePackagesFiled = null; + private static Field publicSourceDirField = null; + + private static void isResourceCanPatch(Context context) throws Throwable { + // - Replace mResDir to point to the external resource file instead of the .apk. This is + // used as the asset path for new Resources objects. + // - Set Application#mLoadedApk to the found LoadedApk instance + + // Find the ActivityThread instance for the current thread + Class activityThread = Class.forName("android.app.ActivityThread"); + currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread); + + // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. + Class loadedApkClass; + try { + loadedApkClass = Class.forName("android.app.LoadedApk"); + } catch (ClassNotFoundException e) { + loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); + } + + + resDir = loadedApkClass.getDeclaredField("mResDir"); + resDir.setAccessible(true); + packagesFiled = activityThread.getDeclaredField("mPackages"); + packagesFiled.setAccessible(true); + + resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages"); + resourcePackagesFiled.setAccessible(true); + + // Create a new AssetManager instance and point it to the resources + AssetManager assets = context.getAssets(); + // Baidu os + if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) { + Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager"); + newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance(); + } else { + newAssetManager = AssetManager.class.getConstructor().newInstance(); + } + + addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); + addAssetPathMethod.setAccessible(true); + + // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm + // in L, so we do it unconditionally. + ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); + ensureStringBlocksMethod.setAccessible(true); + + // Iterate over all known Resources objects + if (SDK_INT >= KITKAT) { + //pre-N + // Find the singleton instance of ResourcesManager + Class resourcesManagerClass = Class.forName("android.app.ResourcesManager"); + Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance"); + mGetInstance.setAccessible(true); + Object resourcesManager = mGetInstance.invoke(null); + try { + Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources"); + fMActiveResources.setAccessible(true); + ArrayMap> activeResources19 = + (ArrayMap>) fMActiveResources.get(resourcesManager); + references = activeResources19.values(); + } catch (NoSuchFieldException ignore) { + // N moved the resources to mResourceReferences + Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences"); + mResourceReferences.setAccessible(true); + references = (Collection>) mResourceReferences.get(resourcesManager); + } + } else { + Field fMActiveResources = activityThread.getDeclaredField("mActiveResources"); + fMActiveResources.setAccessible(true); + HashMap> activeResources7 = + (HashMap>) fMActiveResources.get(currentActivityThread); + references = activeResources7.values(); + } + // check resource + if (references == null) { + throw new IllegalStateException("resource references is null"); + } + try { + assetsFiled = Resources.class.getDeclaredField("mAssets"); + assetsFiled.setAccessible(true); + } catch (Throwable ignore) { + // N moved the mAssets inside an mResourcesImpl field + resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl"); + resourcesImplFiled.setAccessible(true); + } + try { + publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir"); + } catch (NoSuchFieldException ignore) { + } + } + + /** + * @param context + * @param externalResourceFile + * @throws Throwable + */ + public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable { + isResourceCanPatch(context); + if (externalResourceFile == null) { + return; + } + + for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) { + Object value = field.get(currentActivityThread); + + for (Map.Entry> entry + : ((Map>) value).entrySet()) { + Object loadedApk = entry.getValue().get(); + if (loadedApk == null) { + continue; + } + if (externalResourceFile != null) { + resDir.set(loadedApk, externalResourceFile); + } + } + } + // Create a new AssetManager instance and point it to the resources installed under + if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) { + throw new IllegalStateException("Could not create new AssetManager"); + } + + // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm + // in L, so we do it unconditionally. + ensureStringBlocksMethod.invoke(newAssetManager); + + for (WeakReference wr : references) { + Resources resources = wr.get(); + //pre-N + if (resources != null) { + // Set the AssetManager of the Resources instance to our brand new one + try { + assetsFiled.set(resources, newAssetManager); + } catch (Throwable ignore) { + // N + Object resourceImpl = resourcesImplFiled.get(resources); + // for Huawei HwResourcesImpl + Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets"); + implAssets.setAccessible(true); + implAssets.set(resourceImpl, newAssetManager); + } + + clearPreloadTypedArrayIssue(resources); + + resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); + } + } + + // Handle issues caused by WebView on Android N. + // Issue: On Android N, if an activity contains a webview, when screen rotates + // our resource patch may lost effects. + try { + if (publicSourceDirField != null) { + publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile); + } + } catch (Throwable ignore) { + } + + //TODO ignore check +// if (!checkResUpdate(context)) { +// throw new FastdexRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL); +// } + } + + /** + * Why must I do these? + * Resource has mTypedArrayPool field, which just like Message Poll to reduce gc + * MiuiResource change TypedArray to MiuiTypedArray, but it get string block from offset instead of assetManager + */ + private static void clearPreloadTypedArrayIssue(Resources resources) { + // Perform this trick not only in Miui system since we can't predict if any other + // manufacturer would do the same modification to Android. +// if (!isMiuiSystem) { +// return; +// } + Log.w(TAG, "try to clear typedArray cache!"); + // Clear typedArray cache. + try { + Field typedArrayPoolField = ShareReflectUtil.findField(Resources.class, "mTypedArrayPool"); + + final Object origTypedArrayPool = typedArrayPoolField.get(resources); + + Field poolField = ShareReflectUtil.findField(origTypedArrayPool, "mPool"); + + final Constructor typedArrayConstructor = origTypedArrayPool.getClass().getConstructor(int.class); + typedArrayConstructor.setAccessible(true); + final int poolSize = ((Object[]) poolField.get(origTypedArrayPool)).length; + final Object newTypedArrayPool = typedArrayConstructor.newInstance(poolSize); + typedArrayPoolField.set(resources, newTypedArrayPool); + } catch (Throwable ignored) { + Log.e(TAG, "clearPreloadTypedArrayIssue failed, ignore error: " + ignored); + } + } + +// private static boolean checkResUpdate(Context context) { +// try { +// Log.e(TAG, "checkResUpdate success, found test resource assets file " + TEST_ASSETS_VALUE); +// context.getAssets().open(TEST_ASSETS_VALUE); +// } catch (Throwable e) { +// Log.e(TAG, "checkResUpdate failed, can't find test resource assets file " + TEST_ASSETS_VALUE + " e:" + e.getMessage()); +// return false; +// } +// return true; +// } +} diff --git a/runtime/src/main/java/fastdex/runtime/loader/SystemClassLoaderAdder.java b/runtime/src/main/java/fastdex/runtime/loader/SystemClassLoaderAdder.java new file mode 100644 index 00000000..683816f0 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/loader/SystemClassLoaderAdder.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2016 THL A29 Limited, a Tencent company. + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.loader; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.os.Build; +import android.util.Log; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.zip.ZipFile; +import dalvik.system.DexFile; +import dalvik.system.PathClassLoader; +import fastdex.runtime.loader.shareutil.SharePatchFileUtil; +import fastdex.runtime.loader.shareutil.ShareReflectUtil; + +/** + * Created by zhangshaowen on 16/3/18. + */ +public class SystemClassLoaderAdder { + public static final String CHECK_DEX_CLASS = "com.tencent.tinker.loader.TinkerTestDexLoad"; + public static final String CHECK_DEX_FIELD = "isPatch"; + private static final String TAG = "Tinker.ClassLoaderAdder"; + private static int sPatchDexCount = 0; + + @SuppressLint("NewApi") + public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List files) + throws Throwable { + Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size()); + + if (!files.isEmpty()) { + ClassLoader classLoader = loader; + if (Build.VERSION.SDK_INT >= 24) { + classLoader = AndroidNClassLoader.inject(loader, application); + } + //because in dalvik, if inner class is not the same classloader with it wrapper class. + //it won't fail at dex2opt + if (Build.VERSION.SDK_INT >= 23) { + V23.install(classLoader, files, dexOptDir); + } else if (Build.VERSION.SDK_INT >= 19) { + V19.install(classLoader, files, dexOptDir); + } else if (Build.VERSION.SDK_INT >= 14) { + V14.install(classLoader, files, dexOptDir); + } else { + V4.install(classLoader, files, dexOptDir); + } + //install done + sPatchDexCount = files.size(); + Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount); +// +// if (!checkDexInstall(classLoader)) { +// //reset patch dex +// SystemClassLoaderAdder.uninstallPatchDex(classLoader); +// throw new FastdexRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL); +// } + } + } + + public static void uninstallPatchDex(ClassLoader classLoader) throws Throwable { + if (sPatchDexCount <= 0) { + return; + } + if (Build.VERSION.SDK_INT >= 14) { + Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); + Object dexPathList = pathListField.get(classLoader); + ShareReflectUtil.reduceFieldArray(dexPathList, "dexElements", sPatchDexCount); + } else { + ShareReflectUtil.reduceFieldArray(classLoader, "mPaths", sPatchDexCount); + ShareReflectUtil.reduceFieldArray(classLoader, "mFiles", sPatchDexCount); + ShareReflectUtil.reduceFieldArray(classLoader, "mZips", sPatchDexCount); + try { + ShareReflectUtil.reduceFieldArray(classLoader, "mDexs", sPatchDexCount); + } catch (Exception e) { + } + } + } + + private static boolean checkDexInstall(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + Class clazz = Class.forName(CHECK_DEX_CLASS, true, classLoader); + Field filed = ShareReflectUtil.findField(clazz, CHECK_DEX_FIELD); + boolean isPatch = (boolean) filed.get(null); + Log.w(TAG, "checkDexInstall result:" + isPatch); + return isPatch; + } + + /** + * Installer for platform versions 23. + */ + private static final class V23 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = ShareReflectUtil.findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + ArrayList suppressedExceptions = new ArrayList(); + ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, + new ArrayList(additionalClassPathEntries), optimizedDirectory, + suppressedExceptions)); + if (suppressedExceptions.size() > 0) { + for (IOException e : suppressedExceptions) { + Log.w(TAG, "Exception in makePathElement", e); + throw e; + } + + } + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makePathElements}. + */ + private static Object[] makePathElements( + Object dexPathList, ArrayList files, File optimizedDirectory, + ArrayList suppressedExceptions) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + + Method makePathElements; + try { + makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, + List.class); + } catch (NoSuchMethodException e) { + Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure"); + try { + makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class); + } catch (NoSuchMethodException e1) { + Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure"); + try { + Log.e(TAG, "NoSuchMethodException: try use v19 instead"); + return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions); + } catch (NoSuchMethodException e2) { + Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure"); + throw e2; + } + } + } + + return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); + } + } + + /** + * Installer for platform versions 19. + */ + private static final class V19 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = ShareReflectUtil.findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + ArrayList suppressedExceptions = new ArrayList(); + ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, + new ArrayList(additionalClassPathEntries), optimizedDirectory, + suppressedExceptions)); + if (suppressedExceptions.size() > 0) { + for (IOException e : suppressedExceptions) { + Log.w(TAG, "Exception in makeDexElement", e); + throw e; + } + } + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makeDexElements}. + */ + private static Object[] makeDexElements( + Object dexPathList, ArrayList files, File optimizedDirectory, + ArrayList suppressedExceptions) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + + Method makeDexElements = null; + try { + makeDexElements = ShareReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, + ArrayList.class); + } catch (NoSuchMethodException e) { + Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure"); + try { + makeDexElements = ShareReflectUtil.findMethod(dexPathList, "makeDexElements", List.class, File.class, List.class); + } catch (NoSuchMethodException e1) { + Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure"); + throw e1; + } + } + + return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); + } + } + + /** + * Installer for platform versions 14, 15, 16, 17 and 18. + */ + private static final class V14 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = ShareReflectUtil.findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, + new ArrayList(additionalClassPathEntries), optimizedDirectory)); + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makeDexElements}. + */ + private static Object[] makeDexElements( + Object dexPathList, ArrayList files, File optimizedDirectory) + throws IllegalAccessException, InvocationTargetException, + NoSuchMethodException { + Method makeDexElements = + ShareReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); + + return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); + } + } + + /** + * Installer for platform versions 4 to 13. + */ + private static final class V4 { + private static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.DexClassLoader. We modify its + * fields mPaths, mFiles, mZips and mDexs to append additional DEX + * file entries. + */ + int extraSize = additionalClassPathEntries.size(); + + Field pathField = ShareReflectUtil.findField(loader, "path"); + + StringBuilder path = new StringBuilder((String) pathField.get(loader)); + String[] extraPaths = new String[extraSize]; + File[] extraFiles = new File[extraSize]; + ZipFile[] extraZips = new ZipFile[extraSize]; + DexFile[] extraDexs = new DexFile[extraSize]; + for (ListIterator iterator = additionalClassPathEntries.listIterator(); + iterator.hasNext();) { + File additionalEntry = iterator.next(); + String entryPath = additionalEntry.getAbsolutePath(); + path.append(':').append(entryPath); + int index = iterator.previousIndex(); + extraPaths[index] = entryPath; + extraFiles[index] = additionalEntry; + extraZips[index] = new ZipFile(additionalEntry); + //edit by zhangshaowen + String outputPathName = SharePatchFileUtil.optimizedPathFor(additionalEntry, optimizedDirectory); + //for below 4.0, we must input jar or zip + extraDexs[index] = DexFile.loadDex(entryPath, outputPathName, 0); + } + + pathField.set(loader, path.toString()); + ShareReflectUtil.expandFieldArray(loader, "mPaths", extraPaths); + ShareReflectUtil.expandFieldArray(loader, "mFiles", extraFiles); + ShareReflectUtil.expandFieldArray(loader, "mZips", extraZips); + try { + ShareReflectUtil.expandFieldArray(loader, "mDexs", extraDexs); + } catch (Exception e) { + + } + } + } + +} diff --git a/runtime/src/main/java/fastdex/runtime/loader/shareutil/SharePatchFileUtil.java b/runtime/src/main/java/fastdex/runtime/loader/shareutil/SharePatchFileUtil.java new file mode 100644 index 00000000..0e38df10 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/loader/shareutil/SharePatchFileUtil.java @@ -0,0 +1,80 @@ +/* + * Tencent is pleased to support the open source community by making Tinker available. + * + * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.loader.shareutil; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import java.io.File; +import fastdex.common.ShareConstants; +import fastdex.runtime.Constants; + +public class SharePatchFileUtil { + /** + * data dir, such as /data/data/com.github.typ0520.fastdex.sample/fastdex + * @param context + * @return + */ + public static File getFastdexDirectory(Context context) { + ApplicationInfo applicationInfo = context.getApplicationInfo(); + if (applicationInfo == null) { + // Looks like running on a test Context, so just return without patching. + return null; + } + + return new File(applicationInfo.dataDir, Constants.FASTDEX_DIR); + } + + + /** + * data dir, such as /data/data/com.github.typ0520.fastdex.sample/fastdex/patch + * @param context + * @return + */ + public static File getPatchDirectory(Context context) { + return new File(getFastdexDirectory(context), Constants.PATCH_DIR); + } + + public static File getPatchTempDirectory(Context context) { + return new File(getFastdexDirectory(context), Constants.TEMP_DIR); + } + + /** + * change the jar file path as the makeDexElements do + * + * @param path + * @param optimizedDirectory + * @return + */ + public static String optimizedPathFor(File path, File optimizedDirectory) { + String fileName = path.getName(); + if (!fileName.endsWith(ShareConstants.DEX_SUFFIX)) { + int lastDot = fileName.lastIndexOf("."); + if (lastDot < 0) { + fileName += ShareConstants.DEX_SUFFIX; + } else { + StringBuilder sb = new StringBuilder(lastDot + 4); + sb.append(fileName, 0, lastDot); + sb.append(ShareConstants.DEX_SUFFIX); + fileName = sb.toString(); + } + } + + File result = new File(optimizedDirectory, fileName); + return result.getPath(); + } +} + diff --git a/runtime/src/main/java/fastdex/runtime/loader/shareutil/ShareReflectUtil.java b/runtime/src/main/java/fastdex/runtime/loader/shareutil/ShareReflectUtil.java new file mode 100644 index 00000000..81e56b64 --- /dev/null +++ b/runtime/src/main/java/fastdex/runtime/loader/shareutil/ShareReflectUtil.java @@ -0,0 +1,185 @@ +/* + * Tencent is pleased to support the open source community by making Tinker available. + * + * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fastdex.runtime.loader.shareutil; + +import android.content.Context; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Created by zhangshaowen on 16/8/22. + */ +public class ShareReflectUtil { + + /** + * Locates a given field anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the field into. + * @param name field name + * @return a field object + * @throws NoSuchFieldException if the field cannot be located + */ + public static Field findField(Object instance, String name) throws NoSuchFieldException { + for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Field field = clazz.getDeclaredField(name); + + if (!field.isAccessible()) { + field.setAccessible(true); + } + + return field; + } catch (NoSuchFieldException e) { + // ignore and search next + } + } + + throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); + } + + public static Field findField(Class originClazz, String name) throws NoSuchFieldException { + for (Class clazz = originClazz; clazz != null; clazz = clazz.getSuperclass()) { + try { + Field field = clazz.getDeclaredField(name); + + if (!field.isAccessible()) { + field.setAccessible(true); + } + + return field; + } catch (NoSuchFieldException e) { + // ignore and search next + } + } + + throw new NoSuchFieldException("Field " + name + " not found in " + originClazz); + } + + /** + * Locates a given method anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the method into. + * @param name method name + * @param parameterTypes method parameter types + * @return a method object + * @throws NoSuchMethodException if the method cannot be located + */ + public static Method findMethod(Object instance, String name, Class... parameterTypes) + throws NoSuchMethodException { + for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Method method = clazz.getDeclaredMethod(name, parameterTypes); + + if (!method.isAccessible()) { + method.setAccessible(true); + } + + return method; + } catch (NoSuchMethodException e) { + // ignore and search next + } + } + + throw new NoSuchMethodException("Method " + + name + + " with parameters " + + Arrays.asList(parameterTypes) + + " not found in " + instance.getClass()); + } + + /** + * Replace the value of a field containing a non null array, by a new array containing the + * elements of the original array plus the elements of extraElements. + * + * @param instance the instance whose field is to be modified. + * @param fieldName the field to modify. + * @param extraElements elements to append at the end of the array. + */ + public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + Field jlrField = findField(instance, fieldName); + + Object[] original = (Object[]) jlrField.get(instance); + Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length); + + // NOTE: changed to copy extraElements first, for patch load first + + System.arraycopy(extraElements, 0, combined, 0, extraElements.length); + System.arraycopy(original, 0, combined, extraElements.length, original.length); + + jlrField.set(instance, combined); + } + + /** + * Replace the value of a field containing a non null array, by a new array containing the + * elements of the original array plus the elements of extraElements. + * + * @param instance the instance whose field is to be modified. + * @param fieldName the field to modify. + */ + public static void reduceFieldArray(Object instance, String fieldName, int reduceSize) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + if (reduceSize <= 0) { + return; + } + + Field jlrField = findField(instance, fieldName); + + Object[] original = (Object[]) jlrField.get(instance); + int finalLength = original.length - reduceSize; + + if (finalLength <= 0) { + return; + } + + Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), finalLength); + + System.arraycopy(original, reduceSize, combined, 0, finalLength); + + jlrField.set(instance, combined); + } + + public static Object getActivityThread(Context context, + Class activityThread) { + try { + if (activityThread == null) { + activityThread = Class.forName("android.app.ActivityThread"); + } + Method m = activityThread.getMethod("currentActivityThread"); + m.setAccessible(true); + Object currentActivityThread = m.invoke(null); + if (currentActivityThread == null && context != null) { + // In older versions of Android (prior to frameworks/base 66a017b63461a22842) + // the currentActivityThread was built on thread locals, so we'll need to try + // even harder + Field mLoadedApk = context.getClass().getField("mLoadedApk"); + mLoadedApk.setAccessible(true); + Object apk = mLoadedApk.get(context); + Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread"); + mActivityThreadField.setAccessible(true); + currentActivityThread = mActivityThreadField.get(apk); + } + return currentActivityThread; + } catch (Throwable ignore) { + return null; + } + } + +} diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDex.java b/runtime/src/main/java/fastdex/runtime/multidex/MultiDex.java similarity index 99% rename from runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDex.java rename to runtime/src/main/java/fastdex/runtime/multidex/MultiDex.java index b5778f56..e6465663 100644 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDex.java +++ b/runtime/src/main/java/fastdex/runtime/multidex/MultiDex.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.dx168.fastdex.runtime.multidex; +package fastdex.runtime.multidex; import android.app.Application; import android.content.Context; diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexApplication.java b/runtime/src/main/java/fastdex/runtime/multidex/MultiDexApplication.java similarity index 96% rename from runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexApplication.java rename to runtime/src/main/java/fastdex/runtime/multidex/MultiDexApplication.java index c4cf1df9..f8ba9860 100644 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexApplication.java +++ b/runtime/src/main/java/fastdex/runtime/multidex/MultiDexApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.dx168.fastdex.runtime.multidex; +package fastdex.runtime.multidex; import android.app.Application; import android.content.Context; diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexExtractor.java b/runtime/src/main/java/fastdex/runtime/multidex/MultiDexExtractor.java similarity index 99% rename from runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexExtractor.java rename to runtime/src/main/java/fastdex/runtime/multidex/MultiDexExtractor.java index 8bd376c1..0be9dfca 100644 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/MultiDexExtractor.java +++ b/runtime/src/main/java/fastdex/runtime/multidex/MultiDexExtractor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.dx168.fastdex.runtime.multidex; +package fastdex.runtime.multidex; import android.content.Context; import android.content.SharedPreferences; diff --git a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/ZipUtil.java b/runtime/src/main/java/fastdex/runtime/multidex/ZipUtil.java similarity index 99% rename from runtime/src/main/java/com/dx168/fastdex/runtime/multidex/ZipUtil.java rename to runtime/src/main/java/fastdex/runtime/multidex/ZipUtil.java index 2ac72116..f1de1307 100644 --- a/runtime/src/main/java/com/dx168/fastdex/runtime/multidex/ZipUtil.java +++ b/runtime/src/main/java/fastdex/runtime/multidex/ZipUtil.java @@ -18,7 +18,7 @@ * ZipConstants from android libcore. */ -package com.dx168.fastdex.runtime.multidex; +package fastdex.runtime.multidex; import java.io.File; import java.io.IOException; diff --git a/sample/app/build.gradle b/sample/app/build.gradle index 4ae3abda..24475def 100644 --- a/sample/app/build.gradle +++ b/sample/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' -apply plugin: 'me.tatarka.retrolambda' +//apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.github.typ0520.fastdex' fastdex { @@ -19,7 +19,7 @@ fastdex { //default 4 //当变化的java文件数量大于等于这个值时触发dex merge(随着变化的java文件的增多,补丁打包会越来越慢,dex merge以后当前的状态相当于全量打包以后的状态) //如果依赖的library工程比较多建议设置稍微大一些 - dexMergeThreshold = 6 + dexMergeThreshold = 4 } android { @@ -111,7 +111,7 @@ dependencies { compile 'com.bigkoo:pickerview:2.0.8' - compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' +// compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' // compile 'org.xutils:xutils:3.3.36' // compile 'de.hdodenhof:circleimageview:2.0.0' // compile 'de.hdodenhof:circleimageview:2.0.0' @@ -142,13 +142,13 @@ dependencies { project.afterEvaluate { android.applicationVariants.all { variant -> def variantName = variant.name.capitalize() + def variantOutput = variant.outputs.first() if ("Debug".equals(variantName)) { - } } } -task mytest<< { +task mytest { +} -} \ No newline at end of file diff --git a/sample/app/src/main/AndroidManifest.xml b/sample/app/src/main/AndroidManifest.xml index 9df36e7f..9a9e85ee 100644 --- a/sample/app/src/main/AndroidManifest.xml +++ b/sample/app/src/main/AndroidManifest.xml @@ -9,13 +9,323 @@ android:label="@string/app_name" android:supportsRtl="true"> + android:name="com.dx168.fastdex.sample.MainActivity"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/assets/a.txt b/sample/app/src/main/assets/a.txt new file mode 100644 index 00000000..13c6a913 --- /dev/null +++ b/sample/app/src/main/assets/a.txt @@ -0,0 +1 @@ +wwwwwww \ No newline at end of file diff --git a/sample/app/src/main/java/com/dx168/fastdex/sample/CustomView.java b/sample/app/src/main/java/com/dx168/fastdex/sample/CustomView.java index ca3e2e01..66679649 100644 --- a/sample/app/src/main/java/com/dx168/fastdex/sample/CustomView.java +++ b/sample/app/src/main/java/com/dx168/fastdex/sample/CustomView.java @@ -2,7 +2,6 @@ import android.content.Context; import android.util.AttributeSet; -import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import butterknife.BindView; diff --git a/sample/app/src/main/java/com/dx168/fastdex/sample/MainActivity.java b/sample/app/src/main/java/com/dx168/fastdex/sample/MainActivity.java index 00f5065d..cc38b9f8 100644 --- a/sample/app/src/main/java/com/dx168/fastdex/sample/MainActivity.java +++ b/sample/app/src/main/java/com/dx168/fastdex/sample/MainActivity.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.os.Bundle; import android.util.Log; +import android.view.View; import android.widget.Button; import android.widget.Toast; import com.dx168.fastdex.sample.common.CommonUtils; @@ -11,12 +12,17 @@ import com.dx168.fastdex.sample.javalib.JavaLib; import java.lang.reflect.Field; +import butterknife.BindView; + /** * Created by tong on 17/10/3. */ public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); + @BindView(R.id.tv) + View view; + public static void aa() { } @@ -76,7 +82,7 @@ public void run() { } }; - SampleApplication realApp = (SampleApplication)getApplication(); + //SampleApplication realApp = (SampleApplication)getApplication(); new Runnable(){ @Override diff --git a/sample/app/src/main/java/com/dx168/fastdex/sample/SampleApplication.java b/sample/app/src/main/java/com/dx168/fastdex/sample/SampleApplication.java index 93f653e2..89c75a59 100644 --- a/sample/app/src/main/java/com/dx168/fastdex/sample/SampleApplication.java +++ b/sample/app/src/main/java/com/dx168/fastdex/sample/SampleApplication.java @@ -3,6 +3,8 @@ import android.content.Context; import android.support.multidex.MultiDex; +import java.io.IOException; + /** * Created by tong on 17/10/3. */ diff --git a/sample/app/src/main/res/layout/activity_main.xml b/sample/app/src/main/res/layout/activity_main.xml index 7d75f991..1ee47cce 100644 --- a/sample/app/src/main/res/layout/activity_main.xml +++ b/sample/app/src/main/res/layout/activity_main.xml @@ -10,11 +10,17 @@ android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.dx168.fastdex.sample.MainActivity"> +