| /* |
| * Copyright (C) 2014 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 com.android.manifmerger; |
| |
| import static com.android.manifmerger.PlaceholderHandler.APPLICATION_ID; |
| import static com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver; |
| import static com.android.manifmerger.PlaceholderHandler.PACKAGE_NAME; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.concurrency.Immutable; |
| import com.android.ide.common.blame.SourceFilePosition; |
| import com.android.ide.common.blame.SourcePosition; |
| import com.android.utils.ILogger; |
| import com.android.utils.Pair; |
| import com.android.utils.SdkUtils; |
| import com.android.utils.StdLogger; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NodeList; |
| |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * merges android manifest files, idempotent. |
| */ |
| @Immutable |
| public class ManifestMerger2 { |
| |
| @NonNull |
| private final File mManifestFile; |
| |
| @NonNull |
| private final Map<String, Object> mPlaceHolderValues; |
| |
| @NonNull |
| private final KeyBasedValueResolver<SystemProperty> mSystemPropertyResolver; |
| |
| private final ILogger mLogger; |
| private final ImmutableList<Pair<String, File>> mLibraryFiles; |
| private final ImmutableList<File> mFlavorsAndBuildTypeFiles; |
| private final ImmutableList<Invoker.Feature> mOptionalFeatures; |
| private final MergeType mMergeType; |
| private final Optional<File> mReportFile; |
| |
| private ManifestMerger2( |
| @NonNull ILogger logger, |
| @NonNull File mainManifestFile, |
| @NonNull ImmutableList<Pair<String, File>> libraryFiles, |
| @NonNull ImmutableList<File> flavorsAndBuildTypeFiles, |
| @NonNull ImmutableList<Invoker.Feature> optionalFeatures, |
| @NonNull Map<String, Object> placeHolderValues, |
| @NonNull KeyBasedValueResolver<SystemProperty> systemPropertiesResolver, |
| @NonNull MergeType mergeType, |
| @NonNull Optional<File> reportFile) { |
| this.mSystemPropertyResolver = systemPropertiesResolver; |
| this.mPlaceHolderValues = placeHolderValues; |
| this.mManifestFile = mainManifestFile; |
| this.mLogger = logger; |
| this.mLibraryFiles = libraryFiles; |
| this.mFlavorsAndBuildTypeFiles = flavorsAndBuildTypeFiles; |
| this.mOptionalFeatures = optionalFeatures; |
| this.mMergeType = mergeType; |
| this.mReportFile = reportFile; |
| } |
| |
| /** |
| * Perform high level ordering of files merging and delegates actual merging to |
| * {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)} |
| * |
| * @return the merging activity report. |
| * @throws MergeFailureException if the merging cannot be completed (for instance, if xml |
| * files cannot be loaded). |
| */ |
| private MergingReport merge() throws MergeFailureException { |
| // initiate a new merging report |
| MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger); |
| |
| SelectorResolver selectors = new SelectorResolver(); |
| // load all the libraries xml files up front to have a list of all possible node:selector |
| // values. |
| List<LoadedManifestInfo> loadedLibraryDocuments = |
| loadLibraries(selectors, mergingReportBuilder); |
| |
| // load the main manifest file to do some checking along the way. |
| LoadedManifestInfo loadedMainManifestInfo = load( |
| new ManifestInfo( |
| mManifestFile.getName(), |
| mManifestFile, |
| XmlDocument.Type.MAIN, |
| Optional.<String>absent() /* mainManifestPackageName */), |
| selectors, |
| mergingReportBuilder); |
| |
| // first do we have a package declaration in the main manifest ? |
| Optional<XmlAttribute> mainPackageAttribute = |
| loadedMainManifestInfo.getXmlDocument().getPackage(); |
| if (!mainPackageAttribute.isPresent()) { |
| mergingReportBuilder.addMessage( |
| loadedMainManifestInfo.getXmlDocument().getSourceFile(), |
| MergingReport.Record.Severity.ERROR, |
| String.format( |
| "Main AndroidManifest.xml at %1$s manifest:package attribute " |
| + "is not declared", |
| loadedMainManifestInfo.getXmlDocument().getSourceFile() |
| .print(true))); |
| return mergingReportBuilder.build(); |
| } |
| |
| // perform system property injection |
| performSystemPropertiesInjection(mergingReportBuilder, |
| loadedMainManifestInfo.getXmlDocument()); |
| |
| // force the re-parsing of the xml as elements may have been added through system |
| // property injection. |
| loadedMainManifestInfo = new LoadedManifestInfo(loadedMainManifestInfo, |
| loadedMainManifestInfo.getOriginalPackageName(), |
| loadedMainManifestInfo.getXmlDocument().reparse()); |
| |
| // invariant : xmlDocumentOptional holds the higher priority document and we try to |
| // merge in lower priority documents. |
| Optional<XmlDocument> xmlDocumentOptional = Optional.absent(); |
| for (File inputFile : mFlavorsAndBuildTypeFiles) { |
| mLogger.info("Merging flavors and build manifest %s \n", inputFile.getPath()); |
| LoadedManifestInfo overlayDocument = load( |
| new ManifestInfo(null, inputFile, XmlDocument.Type.OVERLAY, |
| Optional.of(mainPackageAttribute.get().getValue())), |
| selectors, |
| mergingReportBuilder); |
| |
| // check package declaration. |
| Optional<XmlAttribute> packageAttribute = |
| overlayDocument.getXmlDocument().getPackage(); |
| // if both files declare a package name, it should be the same. |
| if (loadedMainManifestInfo.getOriginalPackageName().isPresent() && |
| packageAttribute.isPresent() |
| && !loadedMainManifestInfo.getOriginalPackageName().get().equals( |
| packageAttribute.get().getValue())) { |
| // no suggestion for library since this is actually forbidden to change the |
| // the package name per flavor. |
| String message = mMergeType == MergeType.APPLICATION |
| ? String.format( |
| "Overlay manifest:package attribute declared at %1$s value=(%2$s)\n" |
| + "\thas a different value=(%3$s) " |
| + "declared in main manifest at %4$s\n" |
| + "\tSuggestion: remove the overlay declaration at %5$s " |
| + "\tand place it in the build.gradle:\n" |
| + "\t\tflavorName {\n" |
| + "\t\t\tapplicationId = \"%2$s\"\n" |
| + "\t\t}", |
| packageAttribute.get().printPosition(), |
| packageAttribute.get().getValue(), |
| mainPackageAttribute.get().getValue(), |
| mainPackageAttribute.get().printPosition(), |
| packageAttribute.get().getSourceFile().print(true)) |
| : String.format( |
| "Overlay manifest:package attribute declared at %1$s value=(%2$s)\n" |
| + "\thas a different value=(%3$s) " |
| + "declared in main manifest at %4$s", |
| packageAttribute.get().printPosition(), |
| packageAttribute.get().getValue(), |
| mainPackageAttribute.get().getValue(), |
| mainPackageAttribute.get().printPosition()); |
| mergingReportBuilder.addMessage( |
| overlayDocument.getXmlDocument().getSourceFile(), |
| MergingReport.Record.Severity.ERROR, |
| message); |
| return mergingReportBuilder.build(); |
| } |
| |
| overlayDocument.getXmlDocument().getRootNode().getXml().setAttribute("package", |
| mainPackageAttribute.get().getValue()); |
| xmlDocumentOptional = merge(xmlDocumentOptional, overlayDocument, mergingReportBuilder); |
| |
| if (!xmlDocumentOptional.isPresent()) { |
| return mergingReportBuilder.build(); |
| } |
| } |
| |
| mLogger.info("Merging main manifest %s\n", mManifestFile.getPath()); |
| xmlDocumentOptional = |
| merge(xmlDocumentOptional, loadedMainManifestInfo, mergingReportBuilder); |
| |
| if (!xmlDocumentOptional.isPresent()) { |
| return mergingReportBuilder.build(); |
| } |
| |
| // force main manifest package into resulting merged file when creating a library manifest. |
| if (mMergeType == MergeType.LIBRARY) { |
| // extract the package name... |
| String mainManifestPackageName = loadedMainManifestInfo.getXmlDocument().getRootNode() |
| .getXml().getAttribute("package"); |
| // save it in the selector instance. |
| if (!Strings.isNullOrEmpty(mainManifestPackageName)) { |
| xmlDocumentOptional.get().getRootNode().getXml() |
| .setAttribute("package", mainManifestPackageName); |
| } |
| } |
| for (LoadedManifestInfo libraryDocument : loadedLibraryDocuments) { |
| mLogger.info("Merging library manifest " + libraryDocument.getLocation()); |
| xmlDocumentOptional = merge( |
| xmlDocumentOptional, libraryDocument, mergingReportBuilder); |
| if (!xmlDocumentOptional.isPresent()) { |
| return mergingReportBuilder.build(); |
| } |
| } |
| |
| // done with proper merging phase, now we need to trim unwanted elements, placeholder |
| // substitution and system properties injection. |
| ElementsTrimmer.trim(xmlDocumentOptional.get(), mergingReportBuilder); |
| if (mergingReportBuilder.hasErrors()) { |
| return mergingReportBuilder.build(); |
| } |
| |
| if (!mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)) { |
| // do one last placeholder substitution, this is useful as we don't stop the build |
| // when a library failed a placeholder substitution, but the element might have |
| // been overridden so the problem was transient. However, with the final document |
| // ready, all placeholders values must have been provided. |
| KeyBasedValueResolver<String> placeHolderValueResolver = |
| new MapBasedKeyBasedValueResolver<String>(mPlaceHolderValues); |
| PlaceholderHandler placeholderHandler = new PlaceholderHandler(); |
| placeholderHandler.visit( |
| mMergeType, |
| xmlDocumentOptional.get(), |
| placeHolderValueResolver, |
| mergingReportBuilder); |
| if (mergingReportBuilder.hasErrors()) { |
| return mergingReportBuilder.build(); |
| } |
| } |
| |
| // perform system property injection. |
| performSystemPropertiesInjection(mergingReportBuilder, xmlDocumentOptional.get()); |
| |
| XmlDocument finalMergedDocument = xmlDocumentOptional.get(); |
| PostValidator.validate(finalMergedDocument, mergingReportBuilder); |
| if (mergingReportBuilder.hasErrors()) { |
| finalMergedDocument.getRootNode().addMessage(mergingReportBuilder, |
| MergingReport.Record.Severity.WARNING, |
| "Post merge validation failed"); |
| } |
| |
| // only remove tools annotations if we are packaging an application. |
| if (mOptionalFeatures.contains(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)) { |
| finalMergedDocument = |
| ToolsInstructionsCleaner.cleanToolsReferences(finalMergedDocument, mLogger); |
| } |
| |
| if (mOptionalFeatures.contains(Invoker.Feature.EXTRACT_FQCNS)) { |
| extractFcqns(finalMergedDocument); |
| } |
| |
| if (finalMergedDocument != null) { |
| mergingReportBuilder.setMergedDocument(finalMergedDocument); |
| } |
| |
| MergingReport mergingReport = mergingReportBuilder.build(); |
| StdLogger stdLogger = new StdLogger(StdLogger.Level.INFO); |
| mergingReport.log(stdLogger); |
| stdLogger.verbose(mergingReport.getMergedDocument().get().prettyPrint()); |
| |
| if (mReportFile.isPresent()) { |
| writeReport(mergingReport); |
| } |
| |
| return mergingReport; |
| } |
| |
| /** |
| * Creates the merging report file. |
| * @param mergingReport the merging activities report to serialize. |
| */ |
| private void writeReport(MergingReport mergingReport) { |
| FileWriter fileWriter = null; |
| try { |
| if (!mReportFile.get().getParentFile().exists() |
| && !mReportFile.get().getParentFile().mkdirs()) { |
| mLogger.warning(String.format( |
| "Cannot create %1$s manifest merger report file," |
| + "build will continue but merging activities " |
| + "will not be documented", |
| mReportFile.get().getAbsolutePath())); |
| } else { |
| fileWriter = new FileWriter(mReportFile.get()); |
| mergingReport.getActions().log(fileWriter); |
| } |
| } catch (IOException e) { |
| mLogger.warning(String.format( |
| "Error '%1$s' while writing the merger report file, " |
| + "build can continue but merging activities " |
| + "will not be documented ", |
| e.getMessage())); |
| } finally { |
| if (fileWriter != null) { |
| try { |
| fileWriter.close(); |
| } catch (IOException e) { |
| mLogger.warning(String.format( |
| "Error '%1$s' while closing the merger report file, " |
| + "build can continue but merging activities " |
| + "will not be documented ", |
| e.getMessage())); |
| } |
| } |
| } |
| } |
| |
| /** |
| * shorten all fully qualified class name that belong to the same package as the manifest's |
| * package attribute value. |
| * @param finalMergedDocument the AndroidManifest.xml document. |
| */ |
| private void extractFcqns(XmlDocument finalMergedDocument) { |
| extractFcqns(finalMergedDocument.getPackageName(), finalMergedDocument.getRootNode()); |
| } |
| |
| /** |
| * shorten recursively all attributes that are package dependent of the passed nodes and all |
| * its child nodes. |
| * @param packageName the manifest package name. |
| * @param xmlElement the xml element to process recursively. |
| */ |
| private void extractFcqns(String packageName, XmlElement xmlElement) { |
| for (XmlAttribute xmlAttribute : xmlElement.getAttributes()) { |
| if (xmlAttribute.getModel() !=null && xmlAttribute.getModel().isPackageDependent()) { |
| String value = xmlAttribute.getValue(); |
| if (value != null && value.startsWith(packageName) && |
| value.charAt(packageName.length()) == '.') { |
| xmlAttribute.getXml().setValue(value.substring(packageName.length())); |
| } |
| } |
| } |
| for (XmlElement child : xmlElement.getMergeableElements()) { |
| extractFcqns(packageName, child); |
| } |
| } |
| |
| /** |
| * Load an xml file and perform placeholder substitution |
| * @param manifestInfo the android manifest information like if it is a library, an |
| * overlay or a main manifest file. |
| * @param selectors all the libraries selectors |
| * @param mergingReportBuilder the merging report to store events and errors. |
| * @return a loaded manifest info. |
| * @throws MergeFailureException |
| */ |
| private LoadedManifestInfo load( |
| ManifestInfo manifestInfo, |
| KeyResolver<String> selectors, |
| MergingReport.Builder mergingReportBuilder) throws MergeFailureException { |
| |
| XmlDocument xmlDocument; |
| try { |
| xmlDocument = XmlLoader.load(selectors, |
| mSystemPropertyResolver, |
| manifestInfo.mName, |
| manifestInfo.mLocation, |
| manifestInfo.getType(), |
| manifestInfo.getMainManifestPackageName()); |
| } catch (Exception e) { |
| throw new MergeFailureException(e); |
| } |
| |
| String originalPackageName = xmlDocument.getPackageName(); |
| MergingReport.Builder builder = manifestInfo.getType() == XmlDocument.Type.MAIN |
| ? mergingReportBuilder |
| : new MergingReport.Builder(mergingReportBuilder.getLogger()); |
| |
| builder.getActionRecorder().recordDefaultNodeAction( |
| xmlDocument.getRootNode()); |
| |
| // perform place holder substitution, this is necessary to do so early in case placeholders |
| // are used in key attributes. |
| performPlaceHolderSubstitution(manifestInfo, xmlDocument, builder); |
| |
| return new LoadedManifestInfo(manifestInfo, |
| Optional.fromNullable(originalPackageName), xmlDocument); |
| } |
| |
| private void performPlaceHolderSubstitution(ManifestInfo manifestInfo, |
| XmlDocument xmlDocument, |
| MergingReport.Builder mergingReportBuilder) { |
| |
| if (mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)) { |
| return; |
| } |
| |
| // check for placeholders presence, switch first the packageName and application id if |
| // it is not explicitly set. |
| Map<String, Object> finalPlaceHolderValues = mPlaceHolderValues; |
| if (!mPlaceHolderValues.containsKey(PlaceholderHandler.APPLICATION_ID)) { |
| String packageName = manifestInfo.getMainManifestPackageName().isPresent() |
| ? manifestInfo.getMainManifestPackageName().get() |
| : xmlDocument.getPackageName(); |
| // add all existing placeholders except package name that will be swapped. |
| ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder(); |
| for (Map.Entry<String, Object> entry : mPlaceHolderValues.entrySet()) { |
| if (!entry.getKey().equals(PlaceholderHandler.PACKAGE_NAME)) { |
| builder.put(entry); |
| } |
| } |
| builder.put(PlaceholderHandler.PACKAGE_NAME, packageName); |
| if (mMergeType != MergeType.LIBRARY) { |
| builder.put(PlaceholderHandler.APPLICATION_ID, packageName); |
| } |
| finalPlaceHolderValues = builder.build(); |
| } |
| |
| KeyBasedValueResolver<String> placeHolderValueResolver = |
| new MapBasedKeyBasedValueResolver<String>(finalPlaceHolderValues); |
| PlaceholderHandler placeholderHandler = new PlaceholderHandler(); |
| placeholderHandler.visit( |
| mMergeType, |
| xmlDocument, |
| placeHolderValueResolver, |
| mergingReportBuilder); |
| } |
| |
| // merge the optionally existing xmlDocument with a lower priority xml file. |
| private Optional<XmlDocument> merge( |
| Optional<XmlDocument> xmlDocument, |
| LoadedManifestInfo lowerPriorityDocument, |
| MergingReport.Builder mergingReportBuilder) throws MergeFailureException { |
| |
| MergingReport.Result validationResult = PreValidator |
| .validate(mergingReportBuilder, lowerPriorityDocument.getXmlDocument()); |
| if (validationResult == MergingReport.Result.ERROR) { |
| mergingReportBuilder.addMessage( |
| lowerPriorityDocument.getXmlDocument().getSourceFile(), |
| MergingReport.Record.Severity.ERROR, |
| "Validation failed, exiting"); |
| return Optional.absent(); |
| } |
| Optional<XmlDocument> result; |
| if (xmlDocument.isPresent()) { |
| result = xmlDocument.get().merge( |
| lowerPriorityDocument.getXmlDocument(), mergingReportBuilder); |
| } else { |
| mergingReportBuilder.getActionRecorder().recordDefaultNodeAction( |
| lowerPriorityDocument.getXmlDocument().getRootNode()); |
| result = Optional.of(lowerPriorityDocument.getXmlDocument()); |
| } |
| |
| // if requested, dump each intermediary merging stage into the report. |
| if (mOptionalFeatures.contains(Invoker.Feature.KEEP_INTERMEDIARY_STAGES) |
| && result.isPresent()) { |
| mergingReportBuilder.addMergingStage(result.get().prettyPrint()); |
| } |
| |
| return result; |
| } |
| |
| private List<LoadedManifestInfo> loadLibraries(SelectorResolver selectors, |
| MergingReport.Builder mergingReportBuilder) throws MergeFailureException { |
| |
| ImmutableList.Builder<LoadedManifestInfo> loadedLibraryDocuments = ImmutableList.builder(); |
| for (Pair<String, File> libraryFile : mLibraryFiles) { |
| mLogger.info("Loading library manifest " + libraryFile.getSecond().getPath()); |
| ManifestInfo manifestInfo = new ManifestInfo(libraryFile.getFirst(), |
| libraryFile.getSecond(), |
| XmlDocument.Type.LIBRARY, Optional.<String>absent()); |
| XmlDocument libraryDocument; |
| try { |
| libraryDocument = XmlLoader.load(selectors, |
| mSystemPropertyResolver, |
| manifestInfo.mName, manifestInfo.mLocation, |
| XmlDocument.Type.LIBRARY, |
| Optional.<String>absent() /* mainManifestPackageName */); |
| } catch (Exception e) { |
| throw new MergeFailureException(e); |
| } |
| // extract the package name... |
| String libraryPackage = libraryDocument.getRootNode().getXml().getAttribute("package"); |
| // save it in the selector instance. |
| if (!Strings.isNullOrEmpty(libraryPackage)) { |
| selectors.addSelector(libraryPackage, libraryFile.getFirst()); |
| } |
| |
| // perform placeholder substitution, this is useful when the library is using |
| // a placeholder in a key element, we however do not need to record these |
| // substitutions so feed it with a fake merging report. |
| MergingReport.Builder builder = new MergingReport.Builder(mergingReportBuilder.getLogger()); |
| builder.getActionRecorder().recordDefaultNodeAction(libraryDocument.getRootNode()); |
| performPlaceHolderSubstitution(manifestInfo, libraryDocument, builder); |
| if (builder.hasErrors()) { |
| // we log the errors but continue, in case the error is of no consequence |
| // to the application consuming the library. |
| builder.build().log(mLogger); |
| } |
| |
| loadedLibraryDocuments.add(new LoadedManifestInfo(manifestInfo, |
| Optional.fromNullable(libraryDocument.getPackageName()), |
| libraryDocument)); |
| } |
| return loadedLibraryDocuments.build(); |
| } |
| |
| /** |
| * Creates a new {@link com.android.manifmerger.ManifestMerger2.Invoker} instance to invoke |
| * the merging tool to merge manifest files for an application. |
| * |
| * @param mainManifestFile application main manifest file. |
| * @param logger the logger interface to use. |
| * @return an {@link com.android.manifmerger.ManifestMerger2.Invoker} instance that will allow |
| * further customization and trigger the merging tool. |
| */ |
| public static Invoker newMerger(@NonNull File mainManifestFile, |
| @NonNull ILogger logger, |
| @NonNull MergeType mergeType) { |
| return new Invoker(mainManifestFile, logger, mergeType); |
| } |
| |
| /** |
| * List of manifest files properties that can be directly overridden without using a |
| * placeholder. |
| */ |
| public enum SystemProperty implements AutoAddingProperty { |
| |
| /** |
| * Allow setting the merged manifest file package name. |
| */ |
| PACKAGE { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElement(this, actionRecorder, value, document.getRootNode()); |
| } |
| }, |
| /** |
| * http://developer.android.com/guide/topics/manifest/manifest-element.html#vcode |
| */ |
| VERSION_CODE { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElementInAndroidNS(this, actionRecorder, value, document.getRootNode()); |
| } |
| }, |
| /** |
| * http://developer.android.com/guide/topics/manifest/manifest-element.html#vname |
| */ |
| VERSION_NAME { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElementInAndroidNS(this, actionRecorder, value, document.getRootNode()); |
| } |
| }, |
| /** |
| * http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#min |
| */ |
| MIN_SDK_VERSION { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElementInAndroidNS(this, actionRecorder, value, |
| createOrGetUseSdk(actionRecorder, document)); |
| } |
| }, |
| /** |
| * http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#target |
| */ |
| TARGET_SDK_VERSION { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElementInAndroidNS(this, actionRecorder, value, |
| createOrGetUseSdk(actionRecorder, document)); |
| } |
| }, |
| |
| MAX_SDK_VERSION { |
| @Override |
| public void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value) { |
| addToElementInAndroidNS(this, actionRecorder, value, |
| createOrGetUseSdk(actionRecorder, document)); |
| } |
| }; |
| |
| public String toCamelCase() { |
| return SdkUtils.constantNameToCamelCase(name()); |
| } |
| |
| // utility method to add an attribute which name is derived from the enum name(). |
| private static void addToElement( |
| SystemProperty systemProperty, |
| ActionRecorder actionRecorder, |
| String value, |
| XmlElement to) { |
| |
| to.getXml().setAttribute(systemProperty.toCamelCase(), value); |
| XmlAttribute xmlAttribute = new XmlAttribute(to, |
| to.getXml().getAttributeNode(systemProperty.toCamelCase()), null); |
| actionRecorder.recordAttributeAction(xmlAttribute, new Actions.AttributeRecord( |
| Actions.ActionType.INJECTED, |
| new SourceFilePosition(to.getSourceFile(), SourcePosition.UNKNOWN), |
| xmlAttribute.getId(), |
| null, /* reason */ |
| null /* attributeOperationType */)); |
| } |
| |
| // utility method to add an attribute in android namespace which local name is derived from |
| // the enum name(). |
| private static void addToElementInAndroidNS( |
| SystemProperty systemProperty, |
| ActionRecorder actionRecorder, |
| String value, |
| XmlElement to) { |
| |
| String toolsPrefix = getAndroidPrefix(to.getXml()); |
| to.getXml().setAttributeNS(SdkConstants.ANDROID_URI, |
| toolsPrefix + XmlUtils.NS_SEPARATOR + systemProperty.toCamelCase(), |
| value); |
| Attr attr = to.getXml().getAttributeNodeNS(SdkConstants.ANDROID_URI, |
| systemProperty.toCamelCase()); |
| |
| XmlAttribute xmlAttribute = new XmlAttribute(to, attr, null); |
| actionRecorder.recordAttributeAction(xmlAttribute, |
| new Actions.AttributeRecord( |
| Actions.ActionType.INJECTED, |
| new SourceFilePosition(to.getSourceFile(), SourcePosition.UNKNOWN), |
| xmlAttribute.getId(), |
| null, /* reason */ |
| null /* attributeOperationType */ |
| ) |
| ); |
| |
| } |
| |
| // utility method to create or get an existing use-sdk xml element under manifest. |
| // this could be made more generic by adding more metadata to the enum but since there is |
| // only one case so far, keep it simple. |
| private static XmlElement createOrGetUseSdk( |
| ActionRecorder actionRecorder, XmlDocument document) { |
| |
| Element manifest = document.getXml().getDocumentElement(); |
| NodeList usesSdks = manifest |
| .getElementsByTagName(ManifestModel.NodeTypes.USES_SDK.toXmlName()); |
| if (usesSdks.getLength() == 0) { |
| usesSdks = manifest |
| .getElementsByTagNameNS( |
| SdkConstants.ANDROID_URI, |
| ManifestModel.NodeTypes.USES_SDK.toXmlName()); |
| } |
| if (usesSdks.getLength() == 0) { |
| // create it first. |
| Element useSdk = manifest.getOwnerDocument().createElement( |
| ManifestModel.NodeTypes.USES_SDK.toXmlName()); |
| manifest.appendChild(useSdk); |
| XmlElement xmlElement = new XmlElement(useSdk, document); |
| Actions.NodeRecord nodeRecord = new Actions.NodeRecord( |
| Actions.ActionType.INJECTED, |
| new SourceFilePosition(xmlElement.getSourceFile(), |
| SourcePosition.UNKNOWN), |
| xmlElement.getId(), |
| "use-sdk injection requested", |
| NodeOperationType.STRICT); |
| actionRecorder.recordNodeAction(xmlElement, nodeRecord); |
| return xmlElement; |
| } else { |
| return new XmlElement((Element) usesSdks.item(0), document); |
| } |
| } |
| } |
| |
| private static String getAndroidPrefix(Element xml) { |
| String toolsPrefix = XmlUtils.lookupNamespacePrefix( |
| xml, SdkConstants.ANDROID_URI, SdkConstants.ANDROID_NS_NAME, false); |
| if (!toolsPrefix.equals(SdkConstants.ANDROID_NS_NAME) && xml.getOwnerDocument() |
| .getDocumentElement().getAttribute("xmlns:" + toolsPrefix) == null) { |
| // this is weird, the document is using "android" prefix but it's not bound |
| // to our namespace. Add the proper xmlns declaration. |
| xml.setAttribute("xmlns:" + toolsPrefix, SdkConstants.ANDROID_URI); |
| } |
| return toolsPrefix; |
| } |
| |
| /** |
| * Defines the merging type expected from the tool. |
| */ |
| public enum MergeType { |
| /** |
| * Application merging type is used when packaging an application with a set of imported |
| * libraries. The resulting merged android manifest is final and is not expected to be |
| * imported in another application. |
| */ |
| APPLICATION, |
| |
| /** |
| * Library merging typee is used when packaging a library. The resulting android manifest |
| * file will not merge in all the imported libraries this library depends on. Also the tools |
| * annotations will not be removed as they can be useful when later importing the resulting |
| * merged android manifest into an application. |
| */ |
| LIBRARY |
| } |
| |
| /** |
| * Defines a property that can add or override itself into an XML document. |
| */ |
| public interface AutoAddingProperty { |
| |
| /** |
| * Add itself (possibly just override the current value) with the passed value |
| * @param actionRecorder to record actions. |
| * @param document the xml document to add itself to. |
| * @param value the value to set of this property. |
| */ |
| void addTo(@NonNull ActionRecorder actionRecorder, |
| @NonNull XmlDocument document, |
| @NonNull String value); |
| } |
| |
| /** |
| * Perform {@link com.android.manifmerger.ManifestMerger2.SystemProperty} injection. |
| * @param mergingReport to log actions and errors. |
| * @param xmlDocument the xml document to inject into. |
| */ |
| protected void performSystemPropertiesInjection( |
| MergingReport.Builder mergingReport, |
| XmlDocument xmlDocument) { |
| for (SystemProperty systemProperty : SystemProperty.values()) { |
| String propertyOverride = mSystemPropertyResolver.getValue(systemProperty); |
| if (propertyOverride != null) { |
| systemProperty.addTo( |
| mergingReport.getActionRecorder(), xmlDocument, propertyOverride); |
| } |
| } |
| } |
| |
| /** |
| * This class will hold all invocation parameters for the manifest merging tool. |
| * |
| * There are broadly three types of input to the merging tool : |
| * <ul> |
| * <li>Build types and flavors overriding manifests</li> |
| * <li>Application main manifest</li> |
| * <li>Library manifest files</li></lib> |
| * </ul> |
| * |
| * Only the main manifest file is a mandatory parameter. |
| * |
| * High level description of the merging will be as follow : |
| * <ol> |
| * <li>Build type and flavors will be merged first in the order they were added. Highest |
| * priority file added first, lowest added last.</li> |
| * <li>Resulting document is merged with lower priority application main manifest file.</li> |
| * <li>Resulting document is merged with each library file manifest file in the order |
| * they were added. Highest priority added first, lowest added last.</li> |
| * <li>Resulting document is returned as results of the merging process.</li> |
| * </ol> |
| * |
| */ |
| public static class Invoker<T extends Invoker<T>>{ |
| |
| protected final File mMainManifestFile; |
| |
| protected final ImmutableMap.Builder<SystemProperty, Object> mSystemProperties = |
| new ImmutableMap.Builder<SystemProperty, Object>(); |
| |
| protected final ILogger mLogger; |
| |
| protected final ImmutableMap.Builder<String, Object> mPlaceholders = |
| new ImmutableMap.Builder<String, Object>(); |
| |
| private final ImmutableList.Builder<Pair<String, File>> mLibraryFilesBuilder = |
| new ImmutableList.Builder<Pair<String, File>>(); |
| private final ImmutableList.Builder<File> mFlavorsAndBuildTypeFiles = |
| new ImmutableList.Builder<File>(); |
| private final ImmutableList.Builder<Feature> mFeaturesBuilder = |
| new ImmutableList.Builder<Feature>(); |
| private final MergeType mMergeType; |
| @Nullable private File mReportFile; |
| |
| /** |
| * Sets a value for a {@link com.android.manifmerger.ManifestMerger2.SystemProperty} |
| * @param override the property to set |
| * @param value the value for the property |
| * @return itself. |
| */ |
| public Invoker setOverride(SystemProperty override, String value) { |
| mSystemProperties.put(override, value); |
| return thisAsT(); |
| } |
| |
| /** |
| * Adds placeholders names and associated values for substitution. |
| * @return itself. |
| */ |
| public Invoker setPlaceHolderValues(Map<String, String> keyValuePairs) { |
| mPlaceholders.putAll(keyValuePairs); |
| return thisAsT(); |
| } |
| |
| /** |
| * Adds a new placeholder name and value for substitution. |
| * @return itself. |
| */ |
| public Invoker setPlaceHolderValue(String placeHolderName, String value) { |
| mPlaceholders.put(placeHolderName, value); |
| return thisAsT(); |
| } |
| |
| /** |
| * Optional behavior of the merging tool can be turned on by setting these Feature. |
| */ |
| public enum Feature { |
| |
| /** |
| * Keep all intermediary merged files during the merging process. This is particularly |
| * useful for debugging/tracing purposes. |
| */ |
| KEEP_INTERMEDIARY_STAGES, |
| |
| /** |
| * When logging file names, use {@link java.io.File#getName()} rather than |
| * {@link java.io.File#getPath()} |
| */ |
| PRINT_SIMPLE_FILENAMES, |
| |
| /** |
| * Perform a sweep after all merging activities to remove all fully qualified class |
| * names and replace them with the equivalent short version. |
| */ |
| EXTRACT_FQCNS, |
| |
| /** |
| * Perform a sweep after all merging activities to remove all tools: decorations. |
| */ |
| REMOVE_TOOLS_DECLARATIONS, |
| |
| /** |
| * Do no perform placeholders replacement. |
| */ |
| NO_PLACEHOLDER_REPLACEMENT |
| } |
| |
| /** |
| * Creates a new builder with the mandatory main manifest file. |
| * @param mainManifestFile application main manifest file. |
| * @param logger the logger interface to use. |
| */ |
| private Invoker( |
| @NonNull File mainManifestFile, |
| @NonNull ILogger logger, |
| MergeType mergeType) { |
| this.mMainManifestFile = Preconditions.checkNotNull(mainManifestFile); |
| this.mLogger = logger; |
| this.mMergeType = mergeType; |
| } |
| |
| /** |
| * Sets the file to use to write the merging report. If not called, |
| * the merging process will not write a report. |
| * @param mergeReport the file to write the report in. |
| * @return itself. |
| */ |
| public Invoker setMergeReportFile(@NonNull File mergeReport) { |
| mReportFile = mergeReport; |
| return this; |
| } |
| |
| /** |
| * Add one library file manifest, will be added last in the list of library files which will |
| * make the parameter the lowest priority library manifest file. |
| * @param file the library manifest file to add. |
| * @return itself. |
| */ |
| public Invoker addLibraryManifest(File file) { |
| if (mMergeType == MergeType.LIBRARY) { |
| throw new IllegalStateException( |
| "Cannot add library dependencies manifests when creating a library"); |
| } |
| mLibraryFilesBuilder.add(Pair.of(file.getName(), file)); |
| return thisAsT(); |
| } |
| |
| public Invoker addLibraryManifests(List<Pair<String, File>> namesAndFiles) { |
| if (mMergeType == MergeType.LIBRARY && !namesAndFiles.isEmpty()) { |
| throw new IllegalStateException( |
| "Cannot add library dependencies manifests when creating a library"); |
| } |
| mLibraryFilesBuilder.addAll(namesAndFiles); |
| return thisAsT(); |
| } |
| |
| /** |
| * Add several library file manifests at then end of the list which will make them the |
| * lowest priority manifest files. The relative priority between all the files passed as |
| * parameters will be respected. |
| * @param files library manifest files to add last. |
| * @return itself. |
| */ |
| public Invoker addLibraryManifests(File... files) { |
| for (File file : files) { |
| addLibraryManifest(file); |
| } |
| return thisAsT(); |
| } |
| |
| /** |
| * Add a flavor or build type manifest file last in the list. |
| * @param file build type or flavor manifest file |
| * @return itself. |
| */ |
| public Invoker addFlavorAndBuildTypeManifest(File file) { |
| this.mFlavorsAndBuildTypeFiles.add(file); |
| return thisAsT(); |
| } |
| |
| /** |
| * Add several flavor or build type manifest files last in the list. Relative priorities |
| * between the passed files as parameters will be respected. |
| * @param files build type of flavor manifest files to add. |
| * @return itself. |
| */ |
| public Invoker addFlavorAndBuildTypeManifests(File... files) { |
| this.mFlavorsAndBuildTypeFiles.add(files); |
| return thisAsT(); |
| } |
| |
| /** |
| * Sets some optional features for the merge tool. |
| * |
| * @param features one to many features to set. |
| * @return itself. |
| */ |
| public Invoker withFeatures(Feature...features) { |
| mFeaturesBuilder.add(features); |
| return thisAsT(); |
| } |
| |
| /** |
| * Perform the merging and return the result. |
| * |
| * @return an instance of {@link com.android.manifmerger.MergingReport} that will give |
| * access to all the logging and merging records. |
| * |
| * This method can be invoked several time and will re-do the file merges. |
| * |
| * @throws com.android.manifmerger.ManifestMerger2.MergeFailureException if the merging |
| * cannot be completed successfully. |
| */ |
| public MergingReport merge() throws MergeFailureException { |
| |
| // provide some free placeholders values. |
| ImmutableMap<SystemProperty, Object> systemProperties = mSystemProperties.build(); |
| if (systemProperties.containsKey(SystemProperty.PACKAGE)) { |
| // if the package is provided, make it available for placeholder replacement. |
| mPlaceholders.put(PACKAGE_NAME, systemProperties.get(SystemProperty.PACKAGE)); |
| // as well as applicationId since package system property overrides everything |
| // but not when output is a library since only the final (application) |
| // application Id should be used to replace libraries "applicationId" placeholders. |
| if (mMergeType != MergeType.LIBRARY) { |
| mPlaceholders.put(APPLICATION_ID, systemProperties.get(SystemProperty.PACKAGE)); |
| } |
| } |
| |
| ManifestMerger2 manifestMerger = |
| new ManifestMerger2( |
| mLogger, |
| mMainManifestFile, |
| mLibraryFilesBuilder.build(), |
| mFlavorsAndBuildTypeFiles.build(), |
| mFeaturesBuilder.build(), |
| mPlaceholders.build(), |
| new MapBasedKeyBasedValueResolver<SystemProperty>(systemProperties), |
| mMergeType, |
| Optional.fromNullable(mReportFile)); |
| return manifestMerger.merge(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private T thisAsT() { |
| return (T) this; |
| } |
| } |
| |
| /** |
| * Helper class for map based placeholders key value pairs. |
| */ |
| public static class MapBasedKeyBasedValueResolver<T> implements KeyBasedValueResolver<T> { |
| |
| private final ImmutableMap<T, Object> keyValues; |
| |
| public MapBasedKeyBasedValueResolver(Map<T, Object> keyValues) { |
| this.keyValues = ImmutableMap.copyOf(keyValues); |
| } |
| |
| @Nullable |
| @Override |
| public String getValue(@NonNull T key) { |
| Object value = keyValues.get(key); |
| return value == null ? null : value.toString(); |
| } |
| } |
| |
| private static class ManifestInfo { |
| |
| private ManifestInfo( |
| String name, |
| File location, |
| XmlDocument.Type type, |
| Optional<String> mainManifestPackageName) { |
| mName = name; |
| mLocation = location; |
| mType = type; |
| mMainManifestPackageName = mainManifestPackageName; |
| } |
| |
| private final String mName; |
| private final File mLocation; |
| private final XmlDocument.Type mType; |
| private final Optional<String> mMainManifestPackageName; |
| |
| File getLocation() { |
| return mLocation; |
| } |
| |
| XmlDocument.Type getType() { |
| return mType; |
| } |
| |
| Optional<String> getMainManifestPackageName() { |
| return mMainManifestPackageName; |
| } |
| } |
| |
| private static class LoadedManifestInfo extends ManifestInfo { |
| |
| @NonNull private final XmlDocument mXmlDocument; |
| @NonNull private final Optional<String> mOriginalPackageName; |
| |
| private LoadedManifestInfo(@NonNull ManifestInfo manifestInfo, |
| @NonNull Optional<String> originalPackageName, |
| @NonNull XmlDocument xmlDocument) { |
| super(manifestInfo.mName, |
| manifestInfo.mLocation, |
| manifestInfo.mType, |
| manifestInfo.getMainManifestPackageName()); |
| mXmlDocument = xmlDocument; |
| mOriginalPackageName = originalPackageName; |
| } |
| |
| @NonNull |
| public XmlDocument getXmlDocument() { |
| return mXmlDocument; |
| } |
| |
| @NonNull |
| public Optional<String> getOriginalPackageName() { |
| return mOriginalPackageName; |
| } |
| } |
| |
| /** |
| * Implementation a {@link com.android.manifmerger.KeyResolver} capable of resolving all |
| * selectors value in the context of the passed libraries to this merging activities. |
| */ |
| static class SelectorResolver implements KeyResolver<String> { |
| |
| private final Map<String, String> mSelectors = new HashMap<String, String>(); |
| |
| protected void addSelector(String key, String value) { |
| mSelectors.put(key, value); |
| } |
| |
| @Nullable |
| @Override |
| public String resolve(String key) { |
| return mSelectors.get(key); |
| } |
| |
| @Override |
| public Iterable<String> getKeys() { |
| return mSelectors.keySet(); |
| } |
| } |
| |
| // a wrapper exception to all sorts of failure exceptions that can be thrown during merging. |
| public static class MergeFailureException extends Exception { |
| |
| protected MergeFailureException(Exception cause) { |
| super(cause); |
| } |
| } |
| } |