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