View Javadoc
1   /*
2    * Copyright (C) 2009, 2010 Jayway AB
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.simpligility.maven.plugins.android;
17  
18  import com.android.SdkConstants;
19  import com.android.annotations.Nullable;
20  import com.android.repository.Revision;
21  import com.android.sdklib.AndroidTargetHash;
22  import com.android.sdklib.AndroidVersion;
23  import com.android.sdklib.BuildToolInfo;
24  import com.android.sdklib.IAndroidTarget;
25  import com.android.sdklib.repository.AndroidSdkHandler;
26  import com.android.sdklib.repository.targets.AndroidTargetManager;
27  import org.apache.maven.plugin.MojoExecutionException;
28  
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.IOException;
32  import java.util.Properties;
33  
34  /**
35   * Represents an Android SDK.
36   * 
37   * @author hugo.josefson@jayway.com
38   * @author Manfred Moser - manfred@simpligility.com
39   */
40  public class AndroidSdk
41  {
42      /**
43       * the default API level for the SDK used as a fall back if none is supplied, 
44       * should ideally point to the latest available version
45       */
46      private static final String DEFAULT_ANDROID_API_LEVEL = "23";
47      /**
48       * property file in each platform folder with details about platform.
49       */
50      private static final String SOURCE_PROPERTIES_FILENAME = "source.properties";
51      /**
52       * property name for the sdk tools revision in sdk/tools/lib source.properties
53       */
54      private static final String SDK_TOOLS_REVISION_PROPERTY = "Pkg.Revision";
55  
56      /**
57       * folder name for the sdk sub folder that contains the different platform versions.
58       */
59      private static final String PLATFORMS_FOLDER_NAME = "platforms";
60  
61      private static final String BIN_FOLDER_NAME_IN_TOOLS = "bin";
62  
63      private static final String PARAMETER_MESSAGE = "Please provide a proper Android SDK directory path as "
64              + "configuration parameter <sdk><path>...</path></sdk> in the plugin <configuration/>. As an alternative,"
65              + " you may add the parameter to commandline: -Dandroid.sdk.path=... or set environment variable "
66              + AbstractAndroidMojo.ENV_ANDROID_HOME + ".";
67  
68      private final File sdkPath;
69      private File platformToolsPath;
70      private File toolsPath;
71  
72      private final IAndroidTarget androidTarget;
73      private AndroidSdkHandler sdkManager;
74      private int sdkMajorVersion;
75      private String buildToolsVersion;
76      private ProgressIndicatorImpl progressIndicator;
77  
78      public AndroidSdk( File sdkPath, String apiLevel )
79      {
80          this( sdkPath, apiLevel, null );
81      }
82  
83      public AndroidSdk( File sdkPath, String apiLevel, @Nullable String buildToolsVersion )
84      {
85          this.sdkPath = sdkPath;
86          this.buildToolsVersion = buildToolsVersion;
87          this.progressIndicator = new ProgressIndicatorImpl();
88  
89          if ( sdkPath != null )
90          {
91              sdkManager = AndroidSdkHandler.getInstance( sdkPath );
92              platformToolsPath = new File( sdkPath, SdkConstants.FD_PLATFORM_TOOLS );
93              toolsPath = new File( sdkPath, SdkConstants.FD_TOOLS );
94  
95              if ( sdkManager == null )
96              {
97                  throw invalidSdkException( sdkPath, apiLevel );
98              }
99          }
100         loadSDKToolsMajorVersion();
101 
102         if ( apiLevel == null )
103         {
104             apiLevel = DEFAULT_ANDROID_API_LEVEL;
105         }
106 
107         androidTarget = findPlatformByApiLevel( apiLevel );
108         if ( androidTarget == null )
109         {
110             throw invalidSdkException( sdkPath, apiLevel );
111         }
112     }
113 
114     private InvalidSdkException invalidSdkException( File sdkPath, String platformOrApiLevel )
115     {
116         throw new InvalidSdkException( "Invalid SDK: Platform/API level " + platformOrApiLevel
117                 + " not available. This command should give you all you need:\n" + sdkPath.getAbsolutePath()
118                 + File.separator + "tools" + File.separator + "android update sdk --no-ui --obsolete --force" );
119     }
120 
121     private IAndroidTarget findPlatformByApiLevel( String apiLevel )
122     {
123         // try find by api level first
124         AndroidVersion version = null;
125         try
126         {
127             version = new AndroidVersion( apiLevel );
128             String hashString = AndroidTargetHash.getPlatformHashString( version );
129             IAndroidTarget target = sdkManager.getAndroidTargetManager( progressIndicator )
130                     .getTargetFromHashString( hashString, progressIndicator );
131 
132             // SdkManager may return a non-null IAndroidTarget that references nothing.
133             // I suspect it points to an SDK that has been removed.
134             if ( target != null && target.getLocation() != null )
135             {
136                 return target;
137             }
138         }
139         catch ( AndroidVersion.AndroidVersionException ignore )
140         {
141             throw new InvalidSdkException( "Error AndroidVersion: " + ignore.getMessage() );
142         }
143 
144         // fallback to searching for platform on standard Android platforms (isPlatform() is true)
145         for ( IAndroidTarget t: sdkManager.getAndroidTargetManager( null ).getTargets( null ) )
146         {
147             if ( t.isPlatform() && apiLevel.equals( t.getVersionName() ) )
148             {
149                 return t;
150             }
151         }
152         return null;
153     }
154 
155     private void assertPathIsDirectory( final File path )
156     {
157         if ( path == null )
158         {
159             throw new InvalidSdkException( PARAMETER_MESSAGE );
160         }
161         if ( !path.isDirectory() )
162         {
163             throw new InvalidSdkException( "Path \"" + path + "\" is not a directory. " + PARAMETER_MESSAGE );
164         }
165     }
166 
167     /**
168      * Get the aapt tool path.
169      *
170      * @return
171      */
172     public String getAaptPath()
173     {
174         return getPathForBuildTool( BuildToolInfo.PathId.AAPT );
175     }
176 
177     /**
178      * Get the aild tool path
179      * @return
180      */
181     public String getAidlPath()
182     {
183         return getPathForBuildTool( BuildToolInfo.PathId.AIDL );
184     }
185 
186     /**
187      * Get the path for dx.jar
188      * @return
189      */
190     public String getDxJarPath()
191     {
192         return getPathForBuildTool( BuildToolInfo.PathId.DX_JAR );
193     }
194 
195     /**
196      * @return the path to the dx.jar
197      */
198     public String getD8JarPath()
199     {
200         final File pathToDexJar = new File( getPathForBuildTool( BuildToolInfo.PathId.DX_JAR ) );
201         final File pathToD8Jar = new File( pathToDexJar.getParent(), "d8.jar" );
202         return pathToD8Jar.getAbsolutePath();
203     }
204 
205     /**
206      * Get the path for proguard.jar
207      * @return
208      */
209     public String getProguardJarPath()
210     {
211         File directory = new File( getToolsPath(), "proguard" + File.separator + "lib" + File.separator );
212         File proguardJar = new File( directory, "proguard.jar" );
213         if ( proguardJar.exists() ) 
214         {
215             return proguardJar.getAbsolutePath();
216         }
217         throw new InvalidSdkException( "Cannot find " + proguardJar );
218     }
219     
220     /**
221      * Get the path for shrinkedAndroid.jar
222      * @return
223      */
224     public String getShrinkedAndroidJarPath()
225     {
226         File shrinkedAndroidJar = new File( getBuildToolsLibDirectoryPath(), "shrinkedAndroid.jar" );
227         if ( shrinkedAndroidJar.exists() ) 
228         {
229             return shrinkedAndroidJar.getAbsolutePath();
230         }
231         throw new InvalidSdkException( "Cannot find " + shrinkedAndroidJar );
232     }
233     
234     /**
235      * Get the path for build-tools lib directory
236      * @return
237      */
238     public String getBuildToolsLibDirectoryPath()
239     {
240         File buildToolsLib = new File( getBuildToolInfo().getLocation(), "lib" );
241         if ( buildToolsLib.exists() ) 
242         {
243             return buildToolsLib.getAbsolutePath();
244         }
245         throw new InvalidSdkException( "Cannot find " + buildToolsLib );
246     }
247     
248     /**
249      * Get the path for mainDexClasses.rules
250      * @return
251      */
252     public String getMainDexClassesRulesPath()
253     {
254         File mainDexClassesRules = new File( getBuildToolInfo().getLocation(),
255                 "mainDexClasses.rules" );
256         if ( mainDexClassesRules.exists() ) 
257         {
258             return mainDexClassesRules.getAbsolutePath();
259         }
260         throw new InvalidSdkException( "Cannot find " + mainDexClassesRules );
261     }
262 
263     public void assertThatBuildToolsVersionIsAtLeast( String version, String feature ) 
264             throws InvalidSdkException, NumberFormatException 
265     {
266         if ( getBuildToolInfo().getRevision().
267                 compareTo( Revision.parseRevision( version ) ) < 0 )
268         {
269             throw new InvalidSdkException( "Version of build tools must be at least " 
270                     + version + " for " + feature + " to work" );
271         }
272     }
273     
274     /**
275      * Get the android debug tool path (adb).
276      *
277      * @return
278      */
279     public String getAdbPath()
280     {
281         return getPathForPlatformTool( SdkConstants.FN_ADB );
282     }
283 
284     /**
285      * Get the android zipalign path.
286      *
287      * @return
288      */
289     public String getZipalignPath()
290     {
291         return getPathForBuildTool( BuildToolInfo.PathId.ZIP_ALIGN );
292     }
293 
294     /**
295      * Get the android lint path.
296      * 
297      * @return
298      */
299     public String getLintPath()
300     {
301         return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "lint" + ext( ".bat", "" ) );
302     }
303 
304     /**
305      * Get the android monkey runner path.
306      * 
307      * @return
308      */
309     public String getMonkeyRunnerPath()
310     {
311         return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "monkeyrunner" + ext( ".bat", "" ) );
312     }
313 
314     /**
315      * Get the apkbuilder path.
316      *
317      * @return
318      */
319     public String getApkBuilderPath()
320     {
321         return getPathForTool( "apkbuilder" + ext( ".bat", "" ) );
322     }
323 
324     /**
325      * Get the android tool path.
326      *
327      * @return
328      */
329     public String getAndroidPath()
330     {
331         String cmd = "android";
332         String ext = SdkConstants.currentPlatform() == 2 ? ".bat" : "";
333 
334         return getPathForTool( cmd + ext );
335     }
336 
337     /**
338      * Get the path to the tools directory.
339      * @return
340      */
341     public File getToolsPath()
342     {
343         return toolsPath;
344     }
345 
346     private String getPathForBuildTool( BuildToolInfo.PathId pathId )
347     {
348         return getBuildToolInfo().getPath( pathId );
349     }
350     
351     private BuildToolInfo getBuildToolInfo()
352     {
353         //First we use the build tools specified in the pom file
354         if ( buildToolsVersion != null && !buildToolsVersion.equals( "" ) )
355         {
356             BuildToolInfo buildToolInfo = sdkManager.getBuildToolInfo( Revision.parseRevision( buildToolsVersion ),
357                     progressIndicator );
358             if ( buildToolInfo != null )
359             {
360                 return buildToolInfo;
361             }
362             //Since we cannot find the build tool specified by the user we make it fail
363             // instead of using the latest build tool version
364             throw new InvalidSdkException( "Invalid SDK: Build-tools " + buildToolsVersion + " not found."
365                     + " Check your Android SDK to install the build tools " + buildToolsVersion );
366         }
367 
368         if ( androidTarget != null )
369         {
370             BuildToolInfo buildToolInfo = androidTarget.getBuildToolInfo();
371             if ( buildToolInfo != null ) 
372             {
373                 return buildToolInfo;
374             }
375         }
376         // if no valid target is defined, or it has no build tools installed, try to use the latest
377         BuildToolInfo latestBuildToolInfo = sdkManager.getLatestBuildTool( progressIndicator, true );
378         if ( latestBuildToolInfo == null )
379         {
380             throw new InvalidSdkException( "Invalid SDK: Build-tools not found. Check the content of '" 
381                 + sdkPath.getAbsolutePath() + File.separator + "build-tools', or run '" 
382                 + sdkPath.getAbsolutePath() + File.separator + "tools" + File.separator 
383                 + "android sdk' to install them" );
384         }
385         return latestBuildToolInfo;
386     }
387 
388     private String getPathForPlatformTool( String tool )
389     {
390         return new File( platformToolsPath, tool ).getAbsolutePath();
391     }
392 
393     private String getPathForTool( String tool )
394     {
395         return new File( toolsPath, tool ).getAbsolutePath();
396     }
397 
398     private static String ext( String windowsExtension, String nonWindowsExtension )
399     {
400         if ( SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS )
401         {
402             return windowsExtension;
403         }
404         else
405         {
406             return nonWindowsExtension;
407         }
408     }
409 
410     /**
411      * Returns the complete path for <code>framework.aidl</code>, based on this SDK.
412      * 
413      * @return the complete path as a <code>String</code>, including the filename.
414      */
415     public String getPathForFrameworkAidl()
416     {
417         return androidTarget.getPath( IAndroidTarget.ANDROID_AIDL );
418     }
419 
420     /**
421      * Resolves the android.jar from this SDK.
422      * 
423      * @return a <code>File</code> pointing to the android.jar file.
424      * @throws org.apache.maven.plugin.MojoExecutionException
425      *             if the file can not be resolved.
426      */
427     public File getAndroidJar() throws MojoExecutionException
428     {
429         final String androidJarPath = androidTarget.getPath( IAndroidTarget.ANDROID_JAR );
430         if ( androidJarPath == null )
431         {
432             throw new MojoExecutionException( "No AndroidJar found for " + androidTarget.getLocation() );
433         }
434         return new File ( androidJarPath );
435     }
436   
437     /**
438      * Resolves the path for this SDK.
439      * 
440      * @return a <code>File</code> pointing to the SDk Directory.
441      * @throws org.apache.maven.plugin.MojoExecutionException
442      *             if the file can not be resolved.
443      */
444     public File getSdkPath() throws MojoExecutionException
445     {
446         if ( sdkPath.exists() )
447         {
448             return sdkPath;
449         }
450         throw new MojoExecutionException( "Can't find the SDK directory : " + sdkPath.getAbsolutePath() );
451     }
452 
453     /**
454      * This method returns the previously specified version. However, if none have been specified it returns the
455      * "latest" version.
456      */
457     public File getPlatform()
458     {
459         assertPathIsDirectory( sdkPath );
460 
461         final File platformsDirectory = new File( sdkPath, PLATFORMS_FOLDER_NAME );
462         assertPathIsDirectory( platformsDirectory );
463 
464         final File platformDirectory;
465         if ( androidTarget == null )
466         {
467             IAndroidTarget latestTarget = null;
468             AndroidTargetManager targetManager = sdkManager.getAndroidTargetManager( progressIndicator );
469             for ( IAndroidTarget target: targetManager.getTargets( progressIndicator ) )
470             {
471                 if ( target.isPlatform() )
472                 {
473                     if ( latestTarget == null
474                             || target.getVersion().getApiLevel() > latestTarget.getVersion().getApiLevel() )
475                     {
476                         latestTarget = target;
477                     }
478                 }
479             }
480             platformDirectory = new File ( latestTarget.getLocation() );
481         }
482         else
483         {
484             platformDirectory = new File( androidTarget.getLocation() );
485         }
486         assertPathIsDirectory( platformDirectory );
487         return platformDirectory;
488     }
489 
490     /**
491      * Loads the SDK Tools version
492      */
493     private void loadSDKToolsMajorVersion()
494     {
495         File propFile = new File( sdkPath, "tools/" + SOURCE_PROPERTIES_FILENAME );
496         Properties properties = new Properties();
497         try
498         {
499             properties.load( new FileInputStream( propFile ) );
500         }
501         catch ( IOException e )
502         {
503             throw new InvalidSdkException( "Error reading " + propFile.getAbsoluteFile() );
504         }
505 
506         if ( properties.containsKey( SDK_TOOLS_REVISION_PROPERTY ) )
507         {
508             try
509             {
510                 String versionString = properties.getProperty( SDK_TOOLS_REVISION_PROPERTY );
511                 String majorVersion;
512                 if ( versionString.matches( ".*[\\.| ].*" ) )
513                 {
514                     String[] versions = versionString.split( "[\\.| ]" );
515                     majorVersion = versions[ 0 ];
516                 }
517                 else
518                 {
519                     majorVersion = versionString;
520                 }
521                 sdkMajorVersion = Integer.parseInt( majorVersion );
522             }
523             catch ( NumberFormatException e )
524             {
525                 throw new InvalidSdkException( "Error - The property '" + SDK_TOOLS_REVISION_PROPERTY
526                         + "' in the SDK source.properties file  number is not an Integer: "
527                         + properties.getProperty( SDK_TOOLS_REVISION_PROPERTY ) );
528             }
529         }
530     }
531 
532     /**
533      * Returns the version of the SDK Tools.
534      * 
535      * @return
536      */
537     public int getSdkMajorVersion()
538     {
539         return sdkMajorVersion;
540     }
541 }