com.tencent.tinker.build.decoder.ManifestDecoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tinker-patch-lib Show documentation
Show all versions of tinker-patch-lib Show documentation
Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.
/*
* 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 com.tencent.tinker.build.decoder;
import com.tencent.tinker.build.apkparser.AndroidParser;
import com.tencent.tinker.build.patch.Configuration;
import com.tencent.tinker.build.util.Logger;
import com.tencent.tinker.build.util.TinkerPatchException;
import com.tencent.tinker.build.util.TypedValue;
import com.tencent.tinker.build.util.Utils;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Created by zhangshaowen on 16/4/6.
*/
public class ManifestDecoder extends BaseDecoder {
private static final String XML_NODENAME_APPLICATION = "application";
private static final String XML_NODENAME_USES_SDK = "uses-sdk";
private static final String XML_NODEATTR_MIN_SDK_VERSION = "minSdkVersion";
private static final String XML_NODEATTR_TARGET_SDK_VERSION = "targetSdkVersion";
private static final String XML_NODEATTR_PACKAGE = "package";
private static final String XML_NODENAME_ACTIVITY = "activity";
private static final String XML_NODENAME_SERVICE = "service";
private static final String XML_NODENAME_RECEIVER = "receiver";
private static final String XML_NODENAME_PROVIDER = "provider";
private static final String XML_NODEATTR_NAME = "name";
private static final String XML_NODEATTR_EXPORTED = "exported";
private static final String XML_NODEATTR_PROCESS = "process";
private static final String XML_NODENAME_INTENTFILTER = "intent-filter";
public ManifestDecoder(Configuration config) throws IOException {
super(config);
}
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
try {
AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile);
AndroidParser newAndroidManifest = AndroidParser.getAndroidManifest(newFile);
//check minSdkVersion
int minSdkVersion = Integer.parseInt(oldAndroidManifest.apkMeta.getMinSdkVersion());
if (minSdkVersion < TypedValue.ANDROID_40_API_LEVEL) {
if (config.mDexRaw) {
final StringBuilder sb = new StringBuilder();
sb.append("your old apk's minSdkVersion ")
.append(minSdkVersion)
.append(" is below 14, you should set the dexMode to 'jar', ")
.append("otherwise, it will crash at some time");
announceWarningOrException(sb.toString());
}
}
final String oldXml = oldAndroidManifest.xml.trim();
final String newXml = newAndroidManifest.xml.trim();
final boolean isManifestChanged = !oldXml.equals(newXml);
if (!isManifestChanged) {
Logger.d("\nManifest has no changes, skip rest decode works.");
return false;
}
// check whether there is any new Android Component and get their names.
// so far only Activity increment can pass checking.
final Set incActivities = getIncrementActivities(oldAndroidManifest.activities, newAndroidManifest.activities);
final Set incServices = getIncrementServices(oldAndroidManifest.services, newAndroidManifest.services);
final Set incReceivers = getIncrementReceivers(oldAndroidManifest.receivers, newAndroidManifest.receivers);
final Set incProviders = getIncrementProviders(oldAndroidManifest.providers, newAndroidManifest.providers);
final boolean hasIncComponent = (!incActivities.isEmpty() || !incServices.isEmpty()
|| !incProviders.isEmpty() || !incReceivers.isEmpty());
if (!config.mSupportHotplugComponent && hasIncComponent) {
announceWarningOrException("manifest was changed, while hot plug component support mode is disabled. "
+ "Such changes will not take effect, related components: \n"
+ " activity: " + incActivities + "\n"
+ " service: " + incServices + "\n"
+ " receiver: " + incReceivers + "\n"
+ " provider: " + incProviders + "\n"
);
}
// generate increment manifest.
if (hasIncComponent) {
final Document newXmlDoc = DocumentHelper.parseText(newAndroidManifest.xml);
final Document incXmlDoc = DocumentHelper.createDocument();
final Element newRootNode = newXmlDoc.getRootElement();
final String packageName = newRootNode.attributeValue(XML_NODEATTR_PACKAGE);
if (Utils.isNullOrNil(packageName)) {
throw new TinkerPatchException("Unable to find package name from manifest: " + newFile.getAbsolutePath());
}
final Element newAppNode = newRootNode.element(XML_NODENAME_APPLICATION);
final Element incAppNode = incXmlDoc.addElement(newAppNode.getQName());
copyAttributes(newAppNode, incAppNode);
if (!incActivities.isEmpty()) {
final List newActivityNodes = newAppNode.elements(XML_NODENAME_ACTIVITY);
final List incActivityNodes = getIncrementActivityNodes(packageName, newActivityNodes, incActivities);
for (Element node : incActivityNodes) {
incAppNode.add(node.detach());
}
}
if (!incServices.isEmpty()) {
final List newServiceNodes = newAppNode.elements(XML_NODENAME_SERVICE);
final List incServiceNodes = getIncrementServiceNodes(packageName, newServiceNodes, incServices);
for (Element node : incServiceNodes) {
incAppNode.add(node.detach());
}
}
if (!incReceivers.isEmpty()) {
final List newReceiverNodes = newAppNode.elements(XML_NODENAME_RECEIVER);
final List incReceiverNodes = getIncrementReceiverNodes(packageName, newReceiverNodes, incReceivers);
for (Element node : incReceiverNodes) {
incAppNode.add(node.detach());
}
}
if (!incProviders.isEmpty()) {
final List newProviderNodes = newAppNode.elements(XML_NODENAME_PROVIDER);
final List incProviderNodes = getIncrementProviderNodes(packageName, newProviderNodes, incProviders);
for (Element node : incProviderNodes) {
incAppNode.add(node.detach());
}
}
final File incXmlOutput = new File(config.mTempResultDir, TypedValue.INCCOMPONENT_META_FILE);
if (!incXmlOutput.exists()) {
incXmlOutput.getParentFile().mkdirs();
}
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(incXmlOutput));
final XMLWriter docWriter = new XMLWriter(os);
docWriter.write(incXmlDoc);
docWriter.close();
} finally {
Utils.closeQuietly(os);
}
}
if (isManifestChanged && !hasIncComponent) {
Logger.d("\nManifest was changed, while there's no any new components added."
+ " Make sure if such changes were all you expected.\n");
}
} catch (ParseException e) {
e.printStackTrace();
throw new TinkerPatchException("Parse android manifest error!");
} catch (DocumentException e) {
e.printStackTrace();
throw new TinkerPatchException("Parse android manifest by dom4j error!");
} catch (IOException e) {
e.printStackTrace();
throw new TinkerPatchException("Failed to generate increment manifest.", e);
}
return false;
}
private Set getIncrementActivities(Collection oldActivities, Collection newActivities) {
final Set incNames = new HashSet<>(newActivities);
incNames.removeAll(oldActivities);
return incNames;
}
private Set getIncrementServices(Collection oldServices, Collection newServices) {
final Set incNames = new HashSet<>(newServices);
incNames.removeAll(oldServices);
if (!incNames.isEmpty()) {
announceWarningOrException("found added services: " + incNames.toString()
+ "\n currently tinker does not support increase new services, "
+ "such these changes would not take effect.");
}
return incNames;
}
private Set getIncrementReceivers(Collection oldReceivers, Collection newReceivers) {
final Set incNames = new HashSet<>(newReceivers);
incNames.removeAll(oldReceivers);
if (!incNames.isEmpty()) {
announceWarningOrException("found added receivers: " + incNames.toString()
+ "\n currently tinker does not support increase new receivers, "
+ "such these changes would not take effect.");
}
return incNames;
}
private Set getIncrementProviders(Collection oldProviders, Collection newProviders) {
final Set incNames = new HashSet<>(newProviders);
incNames.removeAll(oldProviders);
if (!incNames.isEmpty()) {
announceWarningOrException("found added providers: " + incNames.toString()
+ "\n currently tinker does not support increase new providers, "
+ "such these changes would not take effect.");
}
return incNames;
}
private List getIncrementActivityNodes(String packageName, List newActivityNodes, Collection incActivities) {
final List result = new ArrayList<>();
for (Element newActivityNode : newActivityNodes) {
String activityClazzName = newActivityNode.attributeValue(XML_NODEATTR_NAME);
if (activityClazzName.charAt(0) == '.') {
activityClazzName = packageName + activityClazzName;
}
if (!incActivities.contains(activityClazzName)) {
continue;
}
final String exportedVal = newActivityNode.attributeValue(XML_NODEATTR_EXPORTED,
Utils.isNullOrNil(newActivityNode.elements(XML_NODENAME_INTENTFILTER)) ? "false" : "true");
if ("true".equalsIgnoreCase(exportedVal)) {
announceWarningOrException(
String.format("found a new exported activity %s"
+ ", tinker does not support increase exported activity.", activityClazzName)
);
}
final String processVal = newActivityNode.attributeValue(XML_NODEATTR_PROCESS);
if (processVal != null && processVal.charAt(0) == ':') {
announceWarningOrException(
String.format("found a new activity %s which would be run in standalone process"
+ ", tinker does not support increase such kind of activities.", activityClazzName)
);
}
Logger.d("Found increment activity: " + activityClazzName);
result.add(newActivityNode);
}
return result;
}
private List getIncrementServiceNodes(String packageName, List newServiceNodes, Collection incServices) {
announceWarningOrException("currently tinker does not support increase new services.");
return Collections.emptyList();
}
private List getIncrementReceiverNodes(String packageName, List newReceiverNodes, Collection incReceivers) {
announceWarningOrException("currently tinker does not support increase new receivers.");
return Collections.emptyList();
}
private List getIncrementProviderNodes(String packageName, List newProviderNodes, Collection incProviders) {
announceWarningOrException("currently tinker does not support increase new providers.");
return Collections.emptyList();
}
private void copyAttributes(Element srcNode, Element destNode) {
for (Object attrObj : srcNode.attributes()) {
final Attribute attr = (Attribute) attrObj;
destNode.addAttribute(attr.getQName(), attr.getValue());
}
}
private void announceWarningOrException(String message) {
if (config.mIgnoreWarning) {
final String msg = "Warning:ignoreWarning is true, but " + message;
Logger.e(msg);
} else {
final String msg = "Warning:ignoreWarning is false, " + message;
Logger.e(msg);
throw new TinkerPatchException(msg);
}
}
@Override
public void onAllPatchesStart() throws IOException, TinkerPatchException {
}
@Override
public void onAllPatchesEnd() throws IOException, TinkerPatchException {
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy