/* * ### * Android Maven Plugin - android-maven-plugin * * Copyright (C) 1999 - 2012 Photon Infotech 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. * ### */ /* * Copyright (C) 2009 Jayway AB * * 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.photon.maven.plugins.android.standalonemojos; import java.io.File; import java.io.FileWriter; import java.io.IOException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.w3c.dom.*; import org.xml.sax.SAXException; import com.photon.maven.plugins.android.AbstractAndroidMojo; import com.photon.maven.plugins.android.common.AndroidExtension; import com.photon.maven.plugins.android.configuration.Manifest; /** * Updates various version attributes present in the <code>AndroidManifest.xml</code> file. * * @goal manifest-update * @requiresProject true * @phase process-resources * */ public class ManifestUpdateMojo extends AbstractAndroidMojo { private static final String ATTR_VERSION_NAME = "android:versionName"; private static final String ATTR_VERSION_CODE = "android:versionCode"; private static final String ATTR_SHARED_USER_ID = "android:sharedUserId"; private static final String ATTR_DEBUGGABLE = "android:debuggable"; private static final String ELEM_APPLICATION = "application"; /** * Configuration for the manifest-update goal. * <p> * You can configure this mojo to update the following manifest attributes: * </p> * <p> * <code>android:versionName</code> on the <code>manifest</code> element. * <code>android:versionCode</code> on the <code>manifest</code> element. * <code>android:sharedUserId</code> on the <code>manifest</code> element. * <code>android:debuggable</code> on the <code>application</code> element. * </p> * <p> * Note: This process will reformat the <code>AndroidManifest.xml</code> per JAXP {@link Transformer} defaults if * updates are made to the manifest. * <p> * You can configure attributes in the plugin configuration like so * <pre> * <plugin> * <groupId>com.photon.maven.plugins.android.generation2</groupId> * <artifactId>android-maven-plugin</artifactId> * <executions> * <execution> * <id>update-manifest</id> * <goals> * <goal>manifest-update</goal> * </goals> * <configuration> * <manifest> * <versionName></versionName> * <versionCode>123</versionCode> * <versionCodeAutoIncrement>true|false</versionCodeAutoIncrement> * <versionCodeUpdateFromVersion>true|false</versionCodeUpdateFromVersion> * <sharedUserId>anId</sharedUserId> * <debuggable>true|false</debuggable> * </manifest> * </configuration> * </execution> * </executions> * </plugin> * </pre> * or use properties set in the pom or settings file or supplied as command line parameter. Add "android." in front * of the property name for command line usage. All parameters follow a manifest.* naming convention. * <p> * * @parameter */ private Manifest manifest; /** * Update the <code>android:versionName</code> with the specified parameter. If left empty it * will use the version number of the project. * * @parameter expression="${android.manifest.versionName}" default-value="${project.version}" */ protected String manifestVersionName; /** * Update the <code>android:versionCode</code> attribute with the specified parameter. * * @parameter expression="${android.manifest.versionCode}" */ protected Integer manifestVersionCode; /** * Auto increment the <code>android:versionCode</code> attribute with each build. * * @parameter expression="${android.manifest.versionCodeAutoIncrement}" default-value="false" */ private Boolean manifestVersionCodeAutoIncrement = false; /** * Update the <code>android:versionCode</code> attribute automatically from the project version * e.g 3.0.1 will become version code 301. As described in this blog post * http://www.simpligility.com/2010/11/release-version-management-for-your-android-application/ * but done without using resource filtering. * * @parameter expression="${android.manifest.versionCodeUpdateFromVersion} default-value="false" */ protected Boolean manifestVersionCodeUpdateFromVersion = false; /** * Update the <code>android:sharedUserId</code> attribute with the specified parameter. * * @parameter expression="${android.manifest.sharedUserId}" */ protected String manifestSharedUserId; /** * Update the <code>android:debuggable</code> attribute with the specified parameter. * * @parameter expression="${android.manifest.debuggable}" */ protected Boolean manifestDebuggable; private String parsedVersionName; private Integer parsedVersionCode; private boolean parsedVersionCodeAutoIncrement; private Boolean parsedVersionCodeUpdateFromVersion; private String parsedSharedUserId; private Boolean parsedDebuggable; public void execute() throws MojoExecutionException, MojoFailureException { if (!AndroidExtension.isAndroidPackaging(project.getPackaging())) { return; // skip, not an android project. } if (androidManifestFile == null) { return; // skip, no androidmanifest.xml defined (rare case) } parseConfiguration(); getLog().info("Attempting to update manifest " + androidManifestFile); getLog().debug(" versionName=" + parsedVersionName); getLog().debug(" versionCode=" + parsedVersionCode); getLog().debug(" versionCodeAutoIncrement=" + parsedVersionCodeAutoIncrement); getLog().debug(" versionCodeUpdateFromVersion=" + parsedVersionCodeUpdateFromVersion); getLog().debug(" sharedUserId=" + parsedSharedUserId); getLog().debug(" debuggable=" + parsedDebuggable); if (!androidManifestFile.exists()) { return; // skip, no AndroidManifest.xml file found. } try { updateManifest(androidManifestFile); } catch (IOException e) { throw new MojoFailureException("XML I/O error: " + androidManifestFile, e); } catch (ParserConfigurationException e) { throw new MojoFailureException("Unable to prepare XML parser", e); } catch (SAXException e) { throw new MojoFailureException("Unable to parse XML: " + androidManifestFile, e); } catch (TransformerException e) { throw new MojoFailureException("Unable write XML: " + androidManifestFile, e); } } private void parseConfiguration() { // manifest element found in plugin config in pom if (manifest != null) { if (StringUtils.isNotEmpty(manifest.getVersionName())) { parsedVersionName = manifest.getVersionName(); } else { parsedVersionName = manifestVersionName; } if (manifest.getVersionCode() != null) { parsedVersionCode = manifest.getVersionCode(); } else { parsedVersionCode = manifestVersionCode; } if (manifest.getVersionCodeAutoIncrement() != null) { parsedVersionCodeAutoIncrement = manifest.getVersionCodeAutoIncrement(); } else { parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement; } if (manifest.getVersionCodeUpdateFromVersion() != null) { parsedVersionCodeUpdateFromVersion = manifest.getVersionCodeUpdateFromVersion(); } else { parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion; } if (StringUtils.isNotEmpty(manifest.getSharedUserId())) { parsedSharedUserId = manifest.getSharedUserId(); } else { parsedSharedUserId = manifestSharedUserId; } if (manifest.getDebuggable() != null) { parsedDebuggable = manifest.getDebuggable(); } else { parsedDebuggable = manifestDebuggable; } } else { parsedVersionName = manifestVersionName; parsedVersionCode = manifestVersionCode; parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement; parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion; parsedSharedUserId = manifestSharedUserId; parsedDebuggable = manifestDebuggable; } } /** * Read manifest using JAXP */ private Document readManifest(File manifestFile) throws IOException, ParserConfigurationException, SAXException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(manifestFile); return doc; } /** * Write manifest using JAXP transformer */ private void writeManifest(File manifestFile, Document doc) throws IOException, TransformerException { TransformerFactory xfactory = TransformerFactory.newInstance(); Transformer xformer = xfactory.newTransformer(); xformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); Source source = new DOMSource(doc); FileWriter writer = null; try { writer = new FileWriter(manifestFile, false); String xmldecl = String.format("<?xml version=\"%s\" encoding=\"%s\"?>%n", doc.getXmlVersion(), doc.getXmlEncoding()); writer.write(xmldecl); Result result = new StreamResult(writer); xformer.transform(source, result); } finally { IOUtils.closeQuietly(writer); } } public void updateManifest(File manifestFile) throws IOException, ParserConfigurationException, SAXException, TransformerException, MojoFailureException { Document doc = readManifest(manifestFile); Element manifestElement = doc.getDocumentElement(); boolean dirty = false; if (StringUtils.isEmpty(parsedVersionName)) { // default to ${project.version} parsedVersionName = project.getVersion(); } Attr versionNameAttrib = manifestElement.getAttributeNode(ATTR_VERSION_NAME); if (versionNameAttrib == null || !StringUtils.equals(parsedVersionName, versionNameAttrib.getValue())) { getLog().info("Setting " + ATTR_VERSION_NAME +" to " + parsedVersionName); manifestElement.setAttribute(ATTR_VERSION_NAME, parsedVersionName); dirty = true; } if ((parsedVersionCodeAutoIncrement && parsedVersionCode != null) || (parsedVersionCodeUpdateFromVersion && parsedVersionCode != null) || (parsedVersionCodeAutoIncrement && parsedVersionCodeUpdateFromVersion)) { throw new MojoFailureException("versionCodeAutoIncrement, versionCodeUpdateFromVersion and versionCode " + "are mutual exclusive. They cannot be specified at the same time. " + "Please specify either versionCodeAutoIncrement, versionCodeUpdateFromVersion or versionCode!"); } if (parsedVersionCodeAutoIncrement) { Attr versionCode = manifestElement.getAttributeNode(ATTR_VERSION_CODE); int currentVersionCode = 0; if (versionCode != null) { currentVersionCode = NumberUtils.toInt(versionCode.getValue(), 0); } currentVersionCode++; manifestElement.setAttribute(ATTR_VERSION_CODE, String.valueOf(currentVersionCode)); dirty = true; } if (parsedVersionCodeUpdateFromVersion) { String verString = project.getVersion(); getLog().debug("Generating versionCode for " + verString); ArtifactVersion artifactVersion = new DefaultArtifactVersion(verString); String verCode = Integer.toString(artifactVersion.getMajorVersion()) + Integer.toString(artifactVersion.getMinorVersion()) + Integer.toString(artifactVersion.getIncrementalVersion()); getLog().info("Setting " + ATTR_VERSION_CODE + " to " + verCode); manifestElement.setAttribute(ATTR_VERSION_CODE, verCode); dirty = true; } if (parsedVersionCode != null) { Attr versionCodeAttr = manifestElement.getAttributeNode(ATTR_VERSION_CODE); int currentVersionCode = 0; if (versionCodeAttr != null) { currentVersionCode = NumberUtils.toInt(versionCodeAttr.getValue(), 0); } if (currentVersionCode != parsedVersionCode) { getLog().info("Setting " + ATTR_VERSION_CODE + " to " + parsedVersionCode); manifestElement.setAttribute(ATTR_VERSION_CODE, String.valueOf(parsedVersionCode)); dirty = true; } } if (!StringUtils.isEmpty(parsedSharedUserId)) { Attr sharedUserIdAttrib = manifestElement.getAttributeNode(ATTR_SHARED_USER_ID); if (sharedUserIdAttrib == null || !StringUtils.equals(parsedSharedUserId, sharedUserIdAttrib.getValue())) { getLog().info("Setting " + ATTR_SHARED_USER_ID +" to " + parsedSharedUserId); manifestElement.setAttribute(ATTR_SHARED_USER_ID, parsedSharedUserId); dirty = true; } } if (parsedDebuggable != null) { NodeList appElems = manifestElement.getElementsByTagName(ELEM_APPLICATION); // Update all application nodes. Not sure whether there will ever be more than one. for (int i = 0; i < appElems.getLength(); ++i) { Node node = appElems.item(i); getLog().info("Testing if node " + node.getNodeName() + " is application"); if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element)node; Attr debuggableAttrib = element.getAttributeNode(ATTR_DEBUGGABLE); if (debuggableAttrib == null || parsedDebuggable != BooleanUtils.toBoolean(debuggableAttrib.getValue())) { getLog().info("Setting " + ATTR_DEBUGGABLE + " to " + parsedDebuggable); element.setAttribute(ATTR_DEBUGGABLE, String.valueOf(parsedDebuggable)); dirty = true; } } } } if (dirty) { if (!manifestFile.delete()) { getLog().warn("Could not remove old " + manifestFile); } getLog().info("Made changes to manifest file, updating " + manifestFile); writeManifest(manifestFile, doc); } else { getLog().info("No changes found to write to manifest file"); } } }