View Javadoc

1   package com.jayway.maven.plugins.android.standalonemojos;
2   
3   import com.jayway.maven.plugins.android.AbstractAndroidMojo;
4   import com.jayway.maven.plugins.android.common.AndroidExtension;
5   import com.jayway.maven.plugins.android.common.XmlHelper;
6   import com.jayway.maven.plugins.android.configuration.Manifest;
7   import com.jayway.maven.plugins.android.configuration.UsesSdk;
8   import org.apache.commons.io.IOUtils;
9   import org.apache.commons.lang.BooleanUtils;
10  import org.apache.commons.lang.StringUtils;
11  import org.apache.commons.lang.math.NumberUtils;
12  import org.apache.maven.artifact.versioning.ArtifactVersion;
13  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
14  import org.apache.maven.plugin.MojoExecutionException;
15  import org.apache.maven.plugin.MojoFailureException;
16  import org.w3c.dom.Attr;
17  import org.w3c.dom.Document;
18  import org.w3c.dom.Element;
19  import org.w3c.dom.Node;
20  import org.w3c.dom.NodeList;
21  import org.xml.sax.SAXException;
22  
23  import javax.xml.parsers.DocumentBuilder;
24  import javax.xml.parsers.DocumentBuilderFactory;
25  import javax.xml.parsers.ParserConfigurationException;
26  import javax.xml.transform.OutputKeys;
27  import javax.xml.transform.Result;
28  import javax.xml.transform.Source;
29  import javax.xml.transform.Transformer;
30  import javax.xml.transform.TransformerException;
31  import javax.xml.transform.TransformerFactory;
32  import javax.xml.transform.dom.DOMSource;
33  import javax.xml.transform.stream.StreamResult;
34  import java.io.File;
35  import java.io.FileWriter;
36  import java.io.IOException;
37  import java.util.ArrayList;
38  import java.util.HashSet;
39  import java.util.List;
40  import java.util.Properties;
41  
42  /**
43   * Updates various version attributes present in the <code>AndroidManifest.xml</code> file.
44   *
45   * @author joakim@erdfelt.com
46   * @author nic.strong@gmail.com
47   * @author Manfred Moser <manfred@simpligility.com>
48   * @goal manifest-update
49   * @requiresProject true
50   * @phase process-resources
51   */
52  public class ManifestUpdateMojo extends AbstractAndroidMojo
53  {
54      // basic attributes
55      private static final String ATTR_VERSION_NAME = "android:versionName";
56      private static final String ATTR_VERSION_CODE = "android:versionCode";
57      private static final String ATTR_SHARED_USER_ID = "android:sharedUserId";
58      private static final String ATTR_DEBUGGABLE = "android:debuggable";
59  
60      // supports-screens attributes
61      private static final String ATTR_SCREEN_DENSITY = "android:screenDensity";
62      private static final String ATTR_SCREEN_SIZE = "android:screenSize";
63  
64      // compatible-screens attributes
65      private static final String ATTR_ANY_DENSITY = "android:anyDensity";
66      private static final String ATTR_SMALL_SCREENS = "android:smallScreens";
67      private static final String ATTR_NORMAL_SCREENS = "android:normalScreens";
68      private static final String ATTR_LARGE_SCREENS = "android:largeScreens";
69      private static final String ATTR_XLARGE_SCREENS = "android:xlargeScreens";
70      private static final String ATTR_RESIZEABLE = "android:resizeable";
71      private static final String ATTR_REQUIRES_SMALLEST_WIDTH_DP = "android:requiresSmallestWidthDp";
72      private static final String ATTR_LARGEST_WIDTH_LIMIT_DP = "android:largestWidthLimitDp";
73      private static final String ATTR_COMPATIBLE_WIDTH_LIMIT_DP = "android:compatibleWidthLimitDp";
74  
75      // uses-sdk attributes
76      private static final String ATTR_MIN_SDK_VERSION = "android:minSdkVersion";
77      private static final String ATTR_MAX_SDK_VERSION = "android:maxSdkVersion";
78      private static final String ATTR_TARGET_SDK_VERSION = "android:targetSdkVersion";
79  
80      // provider attributes
81      private static final String ATTR_NAME = "android:name";
82      private static final String ATTR_AUTHORITIES = "android:authorities";
83      // application attributes
84      private static final String ATTR_APPLICATION_ICON = "android:icon";
85      private static final String ATTR_APPLICATION_LABEL = "android:label";
86      private static final String ATTR_APPLICATION_THEME = "android:theme";
87      
88      private static final String ELEM_APPLICATION = "application";
89      private static final String ELEM_PROVIDER = "provider";
90      private static final String ELEM_SUPPORTS_SCREENS = "supports-screens";
91      private static final String ELEM_COMPATIBLE_SCREENS = "compatible-screens";
92      private static final String ELEM_SCREEN = "screen";
93      private static final String ELEM_USES_SDK = "uses-sdk";
94  
95      // version encoding 
96      private static final int INCREMENTAL_VERSION_POSITION = 1;
97      private static final int MINOR_VERSION_POSITION = 1000;
98      private static final int MAJOR_VERSION_POSITION = 1000000;
99  
100     /**
101      * Configuration for the manifest-update goal.
102      * <p>
103      * You can configure this mojo to update the following basic manifest attributes:
104      * </p>
105      * <p>
106      * <code>android:versionName</code> on the <code>manifest</code> element.
107      * <code>android:versionCode</code> on the <code>manifest</code> element.
108      * <code>android:sharedUserId</code> on the <code>manifest</code> element.
109      * <code>android:debuggable</code> on the <code>application</code> element.
110      * </p>
111      * <p>
112      * Moreover, you may specify custom values for the <code>supports-screens</code> and
113      * <code>compatible-screens</code> elements. This is useful if you're using custom build
114      * profiles to build APKs tailored to specific screen configurations. Values passed via POM
115      * configuration for these elements will be merged with whatever is found in the Manifest file.
116      * Values defined in the POM will take precedence.
117      * </p>
118      * <p/>
119      * Note: This process will reformat the <code>AndroidManifest.xml</code> per JAXP
120      * {@link Transformer} defaults if updates are made to the manifest.
121      * <p/>
122      * You can configure attributes in the plugin configuration like so
123      * <p/>
124      * <pre>
125      *   &lt;plugin&gt;
126      *     &lt;groupId&gt;com.jayway.maven.plugins.android.generation2&lt;/groupId&gt;
127      *     &lt;artifactId&gt;android-maven-plugin&lt;/artifactId&gt;
128      *     &lt;executions&gt;
129      *       &lt;execution&gt;
130      *         &lt;id&gt;update-manifest&lt;/id&gt;
131      *         &lt;goals&gt;
132      *           &lt;goal&gt;manifest-update&lt;/goal&gt;
133      *         &lt;/goals&gt;
134      *         &lt;configuration&gt;
135      *           &lt;manifest&gt;
136      *             &lt;versionName&gt;&lt;/versionName&gt;
137      *             &lt;versionCode&gt;123&lt;/versionCode&gt;
138      *             &lt;versionCodeAutoIncrement&gt;true|false&lt;/versionCodeAutoIncrement&gt;
139      *             &lt;versionCodeUpdateFromVersion&gt;true|false&lt;/versionCodeUpdateFromVersion&gt;
140      *             &lt;sharedUserId&gt;anId&lt;/sharedUserId&gt;
141      *             &lt;debuggable&gt;true|false&lt;/debuggable&gt;
142      *
143      *             &lt;supports-screens&gt;
144      *               &lt;anyDensity&gt;true&lt;/anyDensity&gt;
145      *               &lt;xlargeScreens&gt;false&lt;/xlargeScreens&gt;
146      *             &lt;/supports-screens&gt;
147      *
148      *             &lt;compatible-screens&gt;
149      *               &lt;compatible-screen&gt;
150      *                 &lt;screenSize&gt;small&lt;/screenSize&gt;
151      *                 &lt;screenDensity&gt;ldpi&lt;/screenDensity&gt;
152      *               &lt;/compatible-screen&gt;
153      *             &lt;/compatible-screens&gt;
154      *           &lt;/manifest&gt;
155      *         &lt;/configuration&gt;
156      *       &lt;/execution&gt;
157      *     &lt;/executions&gt;
158      *   &lt;/plugin&gt;
159      * </pre>
160      * <p/>
161      * or use properties set in the pom or settings file or supplied as command line parameter. Add
162      * "android." in front of the property name for command line usage. All parameters follow a
163      * manifest.* naming convention.
164      * <p/>
165      *
166      * @parameter
167      */
168     private Manifest manifest;
169 
170     /**
171      * Update the <code>android:versionName</code> with the specified parameter. If left empty it
172      * will use the version number of the project. Exposed via the project property
173      * <code>android.manifest.versionName</code>.
174      *
175      * @parameter expression="${android.manifest.versionName}" default-value="${project.version}"
176      */
177     protected String manifestVersionName;
178 
179     /**
180      * Update the <code>android:versionCode</code> attribute with the specified parameter. Exposed via
181      * the project property <code>android.manifest.versionCode</code>.
182      *
183      * @parameter expression="${android.manifest.versionCode}"
184      */
185     protected Integer manifestVersionCode;
186 
187     /**
188      * Auto increment the <code>android:versionCode</code> attribute with each build. The value is
189      * exposed via the project property <code>android.manifest.versionCodeAutoIncrement</code> and
190      * the resulting value as <code>android.manifest.versionCode</code>.
191      *
192      * @parameter expression="${android.manifest.versionCodeAutoIncrement}" default-value="false"
193      */
194     private Boolean manifestVersionCodeAutoIncrement = false;
195 
196     /**
197      * Update the <code>android:icon</code> attribute with the specified parameter. Exposed via
198      * the project property <code>android.manifest.appIcon</code>.
199      * 
200      * @parameter expression="${android.manifest.applicationIcon}" 
201      */
202     private String manifestApplicationIcon;
203 
204     /**
205      * Update the <code>android:label</code> attribute with the specified parameter. Exposed via
206      * the project property <code>android.manifest.appLabel</code>.
207      * 
208      * @parameter expression="${android.manifest.applicationLabel}" 
209      */
210     private String manifestApplicationLabel;    
211 
212     /**
213      * Update the <code>android:theme</code> attribute with the specified parameter. Exposed via
214      * the project property <code>android.manifest.applicationTheme</code>.
215      * 
216      * @parameter expression="${android.manifest.applicationTheme}" 
217      */
218     private String manifestApplicationTheme;    
219     
220     /**
221      * Update the <code>android:versionCode</code> attribute automatically from the project version
222      * e.g 3.2.1 will become version code 3002001. As described in this blog post
223      * http://www.simpligility.com/2010/11/release-version-management-for-your-android-application/
224      * but done without using resource filtering. The value is exposed via the project property
225      * property <code>android.manifest.versionCodeUpdateFromVersion</code> and the resulting value
226      * as <code>android.manifest.versionCode</code>.
227      * For the purpose of generating the versionCode, if a version element is missing it is presumed to be 0.
228      * The maximum values for the version increment and version minor values are 999,
229      * the version major should be no larger than 2000.  Any other suffixes do not
230      * participate in the version code generation.
231      *
232      * @parameter expression="${android.manifest.versionCodeUpdateFromVersion}" default-value="false"
233      */
234     protected Boolean manifestVersionCodeUpdateFromVersion = false;
235 
236     /**
237      * Update the <code>android:sharedUserId</code> attribute with the specified parameter. If
238      * specified, exposes the project property <code>android.manifest.sharedUserId</code>.
239      *
240      * @parameter expression="${android.manifest.sharedUserId}"
241      */
242     protected String manifestSharedUserId;
243 
244     /**
245      * Update the <code>android:debuggable</code> attribute with the specified parameter. Exposed via
246      * the project property <code>android.manifest.debuggable</code>.
247      *
248      * @parameter expression="${android.manifest.debuggable}"
249      */
250     protected Boolean manifestDebuggable;
251 
252     /**
253      * For a given provider (named by <code>android:name</code> update the <code>android:authorities</code>
254      * attribute for the provider. Exposed via the project property <code>android.manifest.providerAuthorities</code>.
255      *
256      * @parameter expression="${android.manifest.providerAuthorities}"
257      */
258     protected Properties manifestProviderAuthorities;
259 
260     /**
261      *
262      */
263     protected SupportsScreens manifestSupportsScreens;
264 
265     /**
266      *
267      */
268     protected List<CompatibleScreen> manifestCompatibleScreens;
269 
270     /**
271      *  Update the uses-sdk tag. It can be configured to change: <code>android:minSdkVersion</code>,
272      *  <code>android:maxSdkVersion</code> and <code>android:targetSdkVersion</code>
273      */
274     protected UsesSdk manifestUsesSdk;
275 
276     private String parsedVersionName;
277     private Integer parsedVersionCode;
278     private boolean parsedVersionCodeAutoIncrement;
279     private String parsedApplicationIcon;
280     private String parsedApplicationLabel;
281     private String parsedApplicationTheme;
282     private Boolean parsedVersionCodeUpdateFromVersion;
283     private String parsedSharedUserId;
284     private Boolean parsedDebuggable;
285     private SupportsScreens parsedSupportsScreens;
286     private List<CompatibleScreen> parsedCompatibleScreens;
287     private Properties parsedProviderAuthorities;
288     private UsesSdk parsedUsesSdk;
289 
290     /**
291      *
292      * @throws MojoExecutionException
293      * @throws MojoFailureException
294      */
295     public void execute() throws MojoExecutionException, MojoFailureException
296     {
297         if ( ! AndroidExtension.isAndroidPackaging( project.getPackaging() ) )
298         {
299             return; // skip, not an android project.
300         }
301 
302         if ( androidManifestFile == null )
303         {
304             return; // skip, no androidmanifest.xml defined (rare case)
305         }
306 
307         parseConfiguration();
308 
309         getLog().info( "Attempting to update manifest " + androidManifestFile );
310         getLog().debug( "    usesSdk=" + parsedUsesSdk );
311         getLog().debug( "    versionName=" + parsedVersionName );
312         getLog().debug( "    versionCode=" + parsedVersionCode );
313         getLog().debug( "    versionCodeAutoIncrement=" + parsedVersionCodeAutoIncrement );
314         getLog().debug( "    versionCodeUpdateFromVersion=" + parsedVersionCodeUpdateFromVersion );
315         
316         getLog().debug( "    applicationIcon=" + parsedApplicationIcon );
317         getLog().debug( "    applicationLabel=" + parsedApplicationLabel );
318         getLog().debug( "    applicationTheme=" + parsedApplicationTheme );
319         
320         getLog().debug( "    sharedUserId=" + parsedSharedUserId );
321         getLog().debug( "    debuggable=" + parsedDebuggable );
322         getLog().debug( "    providerAuthorities: " + parsedProviderAuthorities );
323         getLog().debug( "    supports-screens: " + ( parsedSupportsScreens == null ? "not set" : "set" ) );
324         getLog().debug( "    compatible-screens: " + ( parsedCompatibleScreens == null ? "not set" : "set" ) );
325 
326         if ( ! androidManifestFile.exists() )
327         {
328             return; // skip, no AndroidManifest.xml file found.
329         }
330 
331         try
332         {
333             updateManifest( androidManifestFile );
334         }
335         catch ( IOException e )
336         {
337             throw new MojoFailureException( "XML I/O error: " + androidManifestFile, e );
338         }
339         catch ( ParserConfigurationException e )
340         {
341             throw new MojoFailureException( "Unable to prepare XML parser", e );
342         }
343         catch ( SAXException e )
344         {
345             throw new MojoFailureException( "Unable to parse XML: " + androidManifestFile, e );
346         }
347         catch ( TransformerException e )
348         {
349             throw new MojoFailureException( "Unable write XML: " + androidManifestFile, e );
350         }
351     }
352 
353     private void parseConfiguration()
354     {
355         // manifest element found in plugin config in pom
356         if ( manifest != null )
357         {
358             if ( StringUtils.isNotEmpty( manifest.getVersionName() ) )
359             {
360                 parsedVersionName = manifest.getVersionName();
361             }
362             else
363             {
364                 parsedVersionName = manifestVersionName;
365             }
366             if ( manifest.getVersionCode() != null )
367             {
368                 parsedVersionCode = manifest.getVersionCode();
369             }
370             else
371             {
372                 parsedVersionCode = manifestVersionCode;
373             }
374             if ( manifest.getVersionCodeAutoIncrement() != null )
375             {
376                 parsedVersionCodeAutoIncrement = manifest.getVersionCodeAutoIncrement();
377             }
378             else
379             {
380                 parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement;
381             }
382             if ( manifest.getVersionCodeUpdateFromVersion() != null )
383             {
384                 parsedVersionCodeUpdateFromVersion = manifest.getVersionCodeUpdateFromVersion();
385             }
386             else
387             {
388                 parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion;
389             }
390             
391             if ( StringUtils.isNotEmpty( manifest.getApplicationIcon() ) ) 
392             {
393                 parsedApplicationIcon = manifest.getApplicationIcon();
394             }
395             else 
396             {
397                 parsedApplicationIcon = manifestApplicationIcon;
398             }
399             
400             if ( StringUtils.isNotEmpty( manifest.getApplicationLabel() ) )  
401             {
402                 parsedApplicationLabel = manifest.getApplicationLabel();
403             }
404             else 
405             {
406                 parsedApplicationLabel = manifestApplicationLabel;
407             }
408             
409             if ( StringUtils.isNotEmpty( manifest.getApplicationTheme() ) )
410             {
411                 parsedApplicationTheme = manifest.getApplicationTheme();
412             }
413             else 
414             {
415                 parsedApplicationTheme = manifestApplicationTheme;
416             }
417 
418             
419             if ( StringUtils.isNotEmpty( manifest.getSharedUserId() ) )
420             {
421                 parsedSharedUserId = manifest.getSharedUserId();
422             }
423             else
424             {
425                 parsedSharedUserId = manifestSharedUserId;
426             }
427             if ( manifest.getDebuggable() != null )
428             {
429                 parsedDebuggable = manifest.getDebuggable();
430             }
431             else
432             {
433                 parsedDebuggable = manifestDebuggable;
434             }
435             if ( manifest.getSupportsScreens() != null )
436             {
437                 parsedSupportsScreens = manifest.getSupportsScreens();
438             }
439             else
440             {
441                 parsedSupportsScreens = manifestSupportsScreens;
442             }
443             if ( manifest.getCompatibleScreens() != null )
444             {
445                 parsedCompatibleScreens = manifest.getCompatibleScreens();
446             }
447             else
448             {
449                 parsedCompatibleScreens = manifestCompatibleScreens;
450             }
451             if ( manifest.getProviderAuthorities() != null )
452             {
453                 parsedProviderAuthorities = manifest.getProviderAuthorities();
454             }
455             else
456             {
457                 parsedProviderAuthorities = manifestProviderAuthorities;
458             }
459             if ( manifest.getUsesSdk() != null )
460             {
461                 parsedUsesSdk = manifest.getUsesSdk();
462             }
463             else
464             {
465                 parsedUsesSdk = manifestUsesSdk;
466             }
467         }
468         else
469         {
470             parsedVersionName = manifestVersionName;
471             parsedVersionCode = manifestVersionCode;
472             parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement;
473             parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion;
474             parsedApplicationIcon = manifestApplicationIcon;
475             parsedApplicationLabel = manifestApplicationLabel;
476             parsedApplicationTheme = manifestApplicationTheme;
477             parsedSharedUserId = manifestSharedUserId;
478             parsedDebuggable = manifestDebuggable;
479             parsedSupportsScreens = manifestSupportsScreens;
480             parsedCompatibleScreens = manifestCompatibleScreens;
481             parsedProviderAuthorities = manifestProviderAuthorities;
482             parsedUsesSdk = manifestUsesSdk;
483         }
484     }
485 
486     /**
487      * Read manifest using JAXP
488      */
489     private Document readManifest( File manifestFile ) throws IOException, ParserConfigurationException, SAXException
490     {
491         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
492         DocumentBuilder db = dbf.newDocumentBuilder();
493         Document doc = db.parse( manifestFile );
494         return doc;
495     }
496 
497     /**
498      * Write manifest using JAXP transformer
499      */
500     private void writeManifest( File manifestFile, Document doc ) throws IOException, TransformerException
501     {
502         TransformerFactory xfactory = TransformerFactory.newInstance();
503         Transformer xformer = xfactory.newTransformer();
504         xformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
505         Source source = new DOMSource( doc );
506 
507         FileWriter writer = null;
508         try
509         {
510             writer = new FileWriter( manifestFile, false );
511             if ( doc.getXmlEncoding() != null && doc.getXmlVersion() != null )
512             {
513                 String xmldecl = String
514                     .format( "<?xml version=\"%s\" encoding=\"%s\"?>%n", doc.getXmlVersion(), doc.getXmlEncoding() );
515                 writer.write( xmldecl );                
516             }
517             Result result = new StreamResult( writer );
518 
519             xformer.transform( source, result );
520         }
521         finally
522         {
523             IOUtils.closeQuietly( writer );
524         }
525     }
526 
527     /**
528      *
529      * @param manifestFile
530      * @throws IOException
531      * @throws ParserConfigurationException
532      * @throws SAXException
533      * @throws TransformerException
534      * @throws MojoFailureException
535      */
536     public void updateManifest( File manifestFile )
537        throws IOException, ParserConfigurationException, SAXException, TransformerException, MojoFailureException
538     {
539         Document doc = readManifest( manifestFile );
540         Element manifestElement = doc.getDocumentElement();
541         boolean dirty = false;
542 
543         if ( StringUtils.isEmpty( parsedVersionName ) )
544         {  // default to ${project.version}
545             parsedVersionName = project.getVersion();
546         }
547         Attr versionNameAttrib = manifestElement.getAttributeNode( ATTR_VERSION_NAME );
548         if ( versionNameAttrib == null || ! StringUtils.equals( parsedVersionName, versionNameAttrib.getValue() ) )
549         {
550             getLog().info( "Setting " + ATTR_VERSION_NAME + " to " + parsedVersionName );
551             manifestElement.setAttribute( ATTR_VERSION_NAME, parsedVersionName );
552             dirty = true;
553         }
554         if ( ( parsedVersionCodeAutoIncrement && parsedVersionCode != null )
555                 || ( parsedVersionCodeUpdateFromVersion && parsedVersionCode != null )
556                 || ( parsedVersionCodeAutoIncrement && parsedVersionCodeUpdateFromVersion ) )
557         {
558             throw new MojoFailureException( "versionCodeAutoIncrement, versionCodeUpdateFromVersion and versionCode "
559                     + "are mutual exclusive. They cannot be specified at the same time. Please specify either "
560                     + "versionCodeAutoIncrement, versionCodeUpdateFromVersion or versionCode!" );
561         }
562         exportProperties();
563         if ( parsedVersionCodeAutoIncrement )
564         {
565             performVersioCodeAutoIncrement( manifestElement );
566             dirty = true;
567         }
568         if ( parsedVersionCodeUpdateFromVersion )
569         {
570             performVersionCodeUpdateFromVersion( manifestElement );
571             dirty = true;
572         }
573         if ( parsedVersionCode != null )
574         {
575             Attr versionCodeAttr = manifestElement.getAttributeNode( ATTR_VERSION_CODE );
576             int currentVersionCode = 0;
577             if ( versionCodeAttr != null )
578             {
579                 currentVersionCode = NumberUtils.toInt( versionCodeAttr.getValue(), 0 );
580             }
581             if ( currentVersionCode != parsedVersionCode )
582             {
583                 getLog().info( "Setting " + ATTR_VERSION_CODE + " to " + parsedVersionCode );
584                 manifestElement.setAttribute( ATTR_VERSION_CODE, String.valueOf( parsedVersionCode ) );
585                 dirty = true;
586             }
587             project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( parsedVersionCode ) );
588         }
589         if ( !StringUtils.isEmpty( parsedApplicationIcon ) ) 
590         {
591             dirty = updateApplicationAttribute( manifestElement, ATTR_APPLICATION_ICON, parsedApplicationIcon, dirty );
592             project.getProperties()
593                 .setProperty( "android.manifest.applicationIcon", String.valueOf( parsedApplicationIcon ) );
594         }
595         
596         if ( ! StringUtils.isEmpty( parsedApplicationLabel ) )
597         {
598             dirty = 
599                 updateApplicationAttribute( manifestElement, ATTR_APPLICATION_LABEL, parsedApplicationLabel, dirty );
600             project.getProperties()
601                 .setProperty( "android.manifest.applicationLabel", String.valueOf( parsedApplicationLabel ) );
602         }
603         
604         if ( ! StringUtils.isEmpty( parsedApplicationTheme ) )
605         {
606             dirty = 
607                 updateApplicationAttribute( manifestElement, ATTR_APPLICATION_THEME, parsedApplicationTheme, dirty );
608             project.getProperties()
609                 .setProperty( "android.manifest.applicationTheme", String.valueOf( parsedApplicationTheme ) );
610         }
611         
612         if ( ! StringUtils.isEmpty( parsedSharedUserId ) )
613         {
614             Attr sharedUserIdAttrib = manifestElement.getAttributeNode( ATTR_SHARED_USER_ID );
615 
616             if ( sharedUserIdAttrib == null || ! StringUtils
617                     .equals( parsedSharedUserId, sharedUserIdAttrib.getValue() ) )
618             {
619                 getLog().info( "Setting " + ATTR_SHARED_USER_ID + " to " + parsedSharedUserId );
620                 manifestElement.setAttribute( ATTR_SHARED_USER_ID, parsedSharedUserId );
621                 dirty = true;
622             }
623         }
624 
625         if ( parsedDebuggable != null )
626         {
627             NodeList appElems = manifestElement.getElementsByTagName( ELEM_APPLICATION );
628 
629             // Update all application nodes. Not sure whether there will ever be more than one.
630             for ( int i = 0; i < appElems.getLength(); ++ i )
631             {
632                 Node node = appElems.item( i );
633                 getLog().info( "Testing if node " + node.getNodeName() + " is application" );
634                 if ( node.getNodeType() == Node.ELEMENT_NODE )
635                 {
636                     Element element = ( Element ) node;
637                     Attr debuggableAttrib = element.getAttributeNode( ATTR_DEBUGGABLE );
638                     if ( debuggableAttrib == null || parsedDebuggable != BooleanUtils
639                             .toBoolean( debuggableAttrib.getValue() ) )
640                     {
641                         getLog().info( "Setting " + ATTR_DEBUGGABLE + " to " + parsedDebuggable );
642                         element.setAttribute( ATTR_DEBUGGABLE, String.valueOf( parsedDebuggable ) );
643                         dirty = true;
644                     }
645                 }
646             }
647         }
648 
649         if ( parsedSupportsScreens != null )
650         {
651             boolean madeDirty = performSupportScreenModification( doc, manifestElement );
652             if ( madeDirty )
653             {
654                 dirty = true;
655             }
656         }
657 
658         if ( parsedCompatibleScreens != null )
659         {
660             getLog().info( "Setting " + ELEM_COMPATIBLE_SCREENS );
661             updateCompatibleScreens( doc, manifestElement );
662             dirty = true;
663         }
664 
665         dirty = processProviderAuthorities( manifestElement, dirty );
666 
667         dirty = processUsesSdk( doc, manifestElement, dirty );
668 
669         if ( dirty )
670         {
671             if ( ! manifestFile.delete() )
672             {
673                 getLog().warn( "Could not remove old " + manifestFile );
674             }
675             getLog().info( "Made changes to manifest file, updating " + manifestFile );
676             writeManifest( manifestFile, doc );
677         }
678         else
679         {
680             getLog().info( "No changes found to write to manifest file" );
681         }
682     }
683 
684     private boolean processProviderAuthorities( Element manifestElement, boolean dirty )
685     {
686         if ( parsedProviderAuthorities != null )
687         {
688             boolean madeDirty = updateProviderAuthorities( manifestElement );
689             if ( madeDirty )
690             {
691                 dirty = true;
692             }
693         }
694         return dirty;
695     }
696 
697     private boolean processUsesSdk( Document doc, Element manifestElement, boolean dirty )
698     {
699         if ( parsedUsesSdk != null )
700         {
701             boolean madeDirty = performUsesSdkModification( doc, manifestElement );
702             if ( madeDirty )
703             {
704                 dirty = true;
705             }
706         }
707         return dirty;
708     }
709 
710     private boolean updateApplicationAttribute( Element manifestElement, 
711             String attribute, String value, boolean dirty )
712     {
713         NodeList appElements = 
714                 manifestElement.getElementsByTagName( ELEM_APPLICATION );
715         // Update all application nodes. Not sure whether there will ever be
716         // more than one.
717         for ( int i = 0; i < appElements.getLength(); ++i )
718         {
719             Node node = appElements.item( i );
720             getLog().info( "Testing if node " + node.getNodeName() 
721                     + " is application" );
722             if ( node.getNodeType() == Node.ELEMENT_NODE )
723             {
724                 Element element = (Element) node;
725                 Attr labelAttrib = element.getAttributeNode( attribute );
726                 if ( labelAttrib == null 
727                         || !value.equals( labelAttrib.getValue() ) )
728                 {
729                     getLog().info( "Setting " + attribute + " to " + value );
730                     element.setAttribute( attribute, String.valueOf( value ) );
731                     dirty = true;
732                 }
733             }
734         }
735         return dirty;
736     }
737 
738     /**
739      * Expose the version properties and other simple parsed manifest entries.
740      */
741     private void exportProperties()
742     {
743         project.getProperties().setProperty( "android.manifest.versionName", parsedVersionName );
744         project.getProperties().setProperty( "android.manifest.versionCodeAutoIncrement",
745                 String.valueOf( parsedVersionCodeAutoIncrement ) );
746         project.getProperties().setProperty( "android.manifest.versionCodeUpdateFromVersion",
747                 String.valueOf( parsedVersionCodeUpdateFromVersion ) );
748         project.getProperties().setProperty( "android.manifest.debuggable", String.valueOf( parsedDebuggable ) );
749         if ( parsedSharedUserId != null )
750         {
751             project.getProperties().setProperty( "android.manifest.sharedUserId", parsedSharedUserId );
752         }
753     }
754 
755     private void performVersioCodeAutoIncrement( Element manifestElement )
756     {
757         Attr versionCode = manifestElement.getAttributeNode( ATTR_VERSION_CODE );
758         int currentVersionCode = 0;
759         if ( versionCode != null )
760         {
761             currentVersionCode = NumberUtils.toInt( versionCode.getValue(), 0 );
762         }
763         currentVersionCode++;
764         manifestElement.setAttribute( ATTR_VERSION_CODE, String.valueOf( currentVersionCode ) );
765         project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( currentVersionCode ) );
766     }
767 
768     /**
769      * If the specified version name cannot be properly parsed then fall back to 
770      * an automatic method.
771      * If the version can be parsed then generate a version code from the
772      * version components.  In an effort to preseve uniqueness two digits
773      * are allowed for both the minor and incremental versions.
774      */
775     private void performVersionCodeUpdateFromVersion( Element manifestElement )
776     {
777         String verString = project.getVersion();
778         getLog().debug( "Generating versionCode for " + verString );
779         ArtifactVersion artifactVersion = new DefaultArtifactVersion( verString );
780         String verCode;
781         if ( artifactVersion.getMajorVersion() < 1 && artifactVersion.getMinorVersion() < 1
782              && artifactVersion.getIncrementalVersion() < 1 )
783         {
784             getLog().warn( "Problem parsing version number occurred. Using fall back to determine version code. " );
785 
786             verCode = verString.replaceAll( "\\D", "" );
787 
788             Attr versionCodeAttr = manifestElement.getAttributeNode( ATTR_VERSION_CODE );
789             int currentVersionCode = 0;
790             if ( versionCodeAttr != null )
791             {
792                 currentVersionCode = NumberUtils.toInt( versionCodeAttr.getValue(), 0 );
793             }
794 
795             if ( Integer.parseInt( verCode ) < currentVersionCode )
796             {
797                 getLog().info( verCode + " < " + currentVersionCode + " so padding versionCode" );
798                 verCode = StringUtils.rightPad( verCode, versionCodeAttr.getValue().length(), "0" );
799             }
800         }
801         else
802         {
803             verCode = Integer.toString( artifactVersion.getMajorVersion() * MAJOR_VERSION_POSITION
804                     + artifactVersion.getMinorVersion() * MINOR_VERSION_POSITION
805                     + artifactVersion.getIncrementalVersion() * INCREMENTAL_VERSION_POSITION );
806         }
807         getLog().info( "Setting " + ATTR_VERSION_CODE + " to " + verCode );
808         manifestElement.setAttribute( ATTR_VERSION_CODE, verCode );
809         project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( verCode ) );
810     }
811 
812     private boolean performSupportScreenModification( Document doc, Element manifestElement )
813     {
814         boolean dirty = false;
815         Element supportsScreensElem = XmlHelper.getOrCreateElement( doc, manifestElement,
816                 ELEM_SUPPORTS_SCREENS );
817 
818         getLog().info( "Setting " + ELEM_SUPPORTS_SCREENS );
819 
820         if ( parsedSupportsScreens.getAnyDensity() != null )
821         {
822             supportsScreensElem.setAttribute( ATTR_ANY_DENSITY, parsedSupportsScreens.getAnyDensity() );
823             dirty = true;
824         }
825         if ( parsedSupportsScreens.getSmallScreens() != null )
826         {
827             supportsScreensElem.setAttribute( ATTR_SMALL_SCREENS, parsedSupportsScreens.getSmallScreens() );
828             dirty = true;
829         }
830         if ( parsedSupportsScreens.getNormalScreens() != null )
831         {
832             supportsScreensElem.setAttribute( ATTR_NORMAL_SCREENS, parsedSupportsScreens.getNormalScreens() );
833             dirty = true;
834         }
835         if ( parsedSupportsScreens.getLargeScreens() != null )
836         {
837             supportsScreensElem.setAttribute( ATTR_LARGE_SCREENS, parsedSupportsScreens.getLargeScreens() );
838             dirty = true;
839         }
840         if ( parsedSupportsScreens.getXlargeScreens() != null )
841         {
842             supportsScreensElem.setAttribute( ATTR_XLARGE_SCREENS, parsedSupportsScreens.getXlargeScreens() );
843             dirty = true;
844         }
845         if ( parsedSupportsScreens.getCompatibleWidthLimitDp() != null )
846         {
847             supportsScreensElem.setAttribute( ATTR_COMPATIBLE_WIDTH_LIMIT_DP,
848                     parsedSupportsScreens.getCompatibleWidthLimitDp() );
849             dirty = true;
850         }
851         if ( parsedSupportsScreens.getLargestWidthLimitDp() != null )
852         {
853             supportsScreensElem
854                     .setAttribute( ATTR_LARGEST_WIDTH_LIMIT_DP, parsedSupportsScreens.getLargestWidthLimitDp() );
855             dirty = true;
856         }
857         if ( parsedSupportsScreens.getRequiresSmallestWidthDp() != null )
858         {
859             supportsScreensElem.setAttribute( ATTR_REQUIRES_SMALLEST_WIDTH_DP,
860                     parsedSupportsScreens.getRequiresSmallestWidthDp() );
861             dirty = true;
862         }
863         if ( parsedSupportsScreens.getResizeable() != null )
864         {
865             supportsScreensElem.setAttribute( ATTR_RESIZEABLE, parsedSupportsScreens.getResizeable() );
866             dirty = true;
867         }
868         return dirty;
869     }
870 
871     private boolean performUsesSdkModification ( Document doc, Element manifestElement )
872     {
873         boolean dirty = false;
874         Element usesSdkElem = XmlHelper.getOrCreateElement( doc, manifestElement,
875                 ELEM_USES_SDK );
876 
877         if ( parsedUsesSdk.getMinSdkVersion() != null )
878         {
879             usesSdkElem.setAttribute( ATTR_MIN_SDK_VERSION, parsedUsesSdk.getMinSdkVersion() );
880             dirty = true;
881         }
882         if ( parsedUsesSdk.getMaxSdkVersion() != null )
883         {
884             usesSdkElem.setAttribute( ATTR_MAX_SDK_VERSION, parsedUsesSdk.getMaxSdkVersion() );
885             dirty = true;
886         }
887         if ( parsedUsesSdk.getTargetSdkVersion() != null )
888         {
889             usesSdkElem.setAttribute( ATTR_TARGET_SDK_VERSION, parsedUsesSdk.getTargetSdkVersion() );
890             dirty = true;
891         }
892 
893         return dirty;
894     }
895 
896     private void updateCompatibleScreens( Document doc, Element manifestElement )
897     {
898         Element compatibleScreensElem = XmlHelper.getOrCreateElement( doc, manifestElement, ELEM_COMPATIBLE_SCREENS );
899 
900         // read those screen elements that were already defined in the Manifest
901         NodeList manifestScreenElems = compatibleScreensElem.getElementsByTagName( ELEM_SCREEN );
902         int numManifestScreens = manifestScreenElems.getLength();
903         ArrayList<CompatibleScreen> manifestScreens = new ArrayList<CompatibleScreen>( numManifestScreens );
904         for ( int i = 0; i < numManifestScreens; i++ )
905         {
906             Element screenElem = ( Element ) manifestScreenElems.item( i );
907 
908             CompatibleScreen screen = new CompatibleScreen();
909             screen.setScreenDensity( screenElem.getAttribute( ATTR_SCREEN_DENSITY ) );
910             screen.setScreenSize( screenElem.getAttribute( ATTR_SCREEN_SIZE ) );
911 
912             manifestScreens.add( screen );
913             getLog().debug( "Found Manifest compatible-screen: " + screen );
914         }
915 
916         // remove all child nodes, since we'll rebuild the element
917         XmlHelper.removeDirectChildren( compatibleScreensElem );
918 
919         for ( CompatibleScreen screen : parsedCompatibleScreens )
920         {
921             getLog().debug( "Found POM compatible-screen: " + screen );
922         }
923 
924         // merge those screens defined in the POM, overriding any matching screens
925         // already defined in the Manifest
926         HashSet<CompatibleScreen> mergedScreens = new HashSet<CompatibleScreen>();
927         mergedScreens.addAll( manifestScreens );
928         mergedScreens.addAll( parsedCompatibleScreens );
929 
930         for ( CompatibleScreen screen : mergedScreens )
931         {
932             getLog().debug( "Using compatible-screen: " + screen );
933             Element screenElem = doc.createElement( ELEM_SCREEN );
934             screenElem.setAttribute( ATTR_SCREEN_SIZE, screen.getScreenSize() );
935             screenElem.setAttribute( ATTR_SCREEN_DENSITY, screen.getScreenDensity() );
936 
937             compatibleScreensElem.appendChild( screenElem );
938         }
939     }
940 
941     private boolean updateProviderAuthorities( Element manifestElement )
942     {
943         boolean dirty = false;
944         NodeList appElems = manifestElement.getElementsByTagName( ELEM_APPLICATION );
945 
946         // Update all application nodes. Not sure whether there will ever be more than one.
947         for ( int i = 0; i < appElems.getLength(); ++ i )
948         {
949             Node node = appElems.item( i );
950             if ( node.getNodeType() == Node.ELEMENT_NODE )
951             {
952                 NodeList providerElems = manifestElement.getElementsByTagName( ELEM_PROVIDER );
953                 for ( int j = 0; j < providerElems.getLength(); ++ j )
954                 {
955                     Node providerNode = providerElems.item( j );
956                     if ( providerNode.getNodeType() == Node.ELEMENT_NODE )
957                     {
958                         Element providerElem = (Element) providerNode;
959                         Attr providerName = providerElem.getAttributeNode( ATTR_NAME );
960                         getLog().debug( "Checking provider " + providerName.getValue() );
961                         if ( shouldPerformProviderUpdate( providerName ) )
962                         {
963                             dirty = true;
964                             String name = providerName.getValue();
965                             String newAuthorities = parsedProviderAuthorities.getProperty( name );
966                             getLog().info( "Updating provider " + name + " authorities attr to " + newAuthorities );
967                             performProviderUpdate( providerElem, newAuthorities );
968                         }
969                     }
970                 }
971             }
972         }
973 
974         return dirty;
975     }
976 
977     private boolean shouldPerformProviderUpdate( Attr providerName )
978     {
979         if ( providerName == null )
980         {
981             return false;
982         }
983 
984         for ( String propName: parsedProviderAuthorities.stringPropertyNames() )
985         {
986             if ( propName.equals( providerName.getValue() ) )
987             {
988                 return true;
989             }
990         }
991         return false;
992     }
993 
994     private void performProviderUpdate( Element providerElem, String newAuthorities )
995     {
996         Attr providerAuthorities = providerElem.getAttributeNode( ATTR_AUTHORITIES );
997         providerAuthorities.setValue( newAuthorities );
998     }
999 
1000 }