1 /* 2 * Copyright (C) 2009 The Android Open Source Project 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 17 package com.android.ant; 18 19 import org.apache.tools.ant.BuildException; 20 import org.apache.tools.ant.Project; 21 import org.apache.tools.ant.taskdefs.ExecTask; 22 import org.apache.tools.ant.types.Path; 23 24 import java.io.File; 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.List; 28 import java.util.Set; 29 30 /** 31 * Task to execute aapt. 32 * 33 * <p>It does not follow the exec task format, instead it has its own parameters, which maps 34 * directly to aapt.</p> 35 * <p>It is able to run aapt several times if library setup requires generating several 36 * R.java files. 37 * <p>The following map shows how to use the task for each supported aapt command line 38 * parameter.</p> 39 * 40 * <table border="1"> 41 * <tr><td><b>Aapt Option</b></td><td><b>Ant Name</b></td><td><b>Type</b></td></tr> 42 * <tr><td>path to aapt</td><td>executable</td><td>attribute (Path)</td> 43 * <tr><td>command</td><td>command</td><td>attribute (String)</td> 44 * <tr><td>-v</td><td>verbose</td><td>attribute (boolean)</td></tr> 45 * <tr><td>-f</td><td>force</td><td>attribute (boolean)</td></tr> 46 * <tr><td>-M AndroidManifest.xml</td><td>manifest</td><td>attribute (Path)</td></tr> 47 * <tr><td>-I base-package</td><td>androidjar</td><td>attribute (Path)</td></tr> 48 * <tr><td>-A asset-source-dir</td><td>assets</td><td>attribute (Path</td></tr> 49 * <tr><td>-S resource-sources</td><td><res path=""></td><td>nested element(s)<br>with attribute (Path)</td></tr> 50 * <tr><td>-0 extension</td><td><nocompress extension=""><br><nocompress></td><td>nested element(s)<br>with attribute (String)</td></tr> 51 * <tr><td>-F apk-file</td><td>apkfolder<br>outfolder<br>apkbasename<br>basename</td><td>attribute (Path)<br>attribute (Path) deprecated<br>attribute (String)<br>attribute (String) deprecated</td></tr> 52 * <tr><td>-J R-file-dir</td><td>rfolder</td><td>attribute (Path)<br>-m always enabled</td></tr> 53 * <tr><td>--rename-manifest-package package-name</td><td>manifestpackage</td><td>attribute (String)</td></tr> 54 * <tr><td></td><td></td><td></td></tr> 55 * </table> 56 */ 57 public final class AaptExecTask extends SingleDependencyTask { 58 59 /** 60 * Class representing a <nocompress> node in the main task XML. 61 * This let the developers prevent compression of some files in assets/ and res/raw/ 62 * by extension. 63 * If the extension is null, this will disable compression for all files in assets/ and 64 * res/raw/ 65 */ 66 public final static class NoCompress { 67 String mExtension; 68 69 /** 70 * Sets the value of the "extension" attribute. 71 * @param extention the extension. 72 */ 73 public void setExtension(String extention) { 74 mExtension = extention; 75 } 76 } 77 78 private String mExecutable; 79 private String mCommand; 80 private boolean mForce = true; // true due to legacy reasons 81 private boolean mDebug = false; 82 private boolean mVerbose = false; 83 private boolean mUseCrunchCache = false; 84 private int mVersionCode = 0; 85 private String mVersionName; 86 private String mManifest; 87 private String mManifestPackage; 88 private ArrayList<Path> mResources; 89 private String mAssets; 90 private String mAndroidJar; 91 private String mApkFolder; 92 private String mApkName; 93 private String mResourceFilter; 94 private String mRFolder; 95 private final ArrayList<NoCompress> mNoCompressList = new ArrayList<NoCompress>(); 96 private String mLibraryResFolderPathRefid; 97 private String mLibraryPackagesRefid; 98 private boolean mNonConstantId; 99 100 /** 101 * Input path that ignores the same folders/files that aapt does. 102 */ 103 private static class ResFolderInputPath extends InputPath { 104 public ResFolderInputPath(File file, Set<String> extensionsToCheck) { 105 super(file, extensionsToCheck); 106 } 107 108 @Override 109 public boolean ignores(File file) { 110 String name = file.getName(); 111 char firstChar = name.charAt(0); 112 113 if (firstChar == '.' || (firstChar == '_' && file.isDirectory()) || 114 name.charAt(name.length()-1) == '~') { 115 return true; 116 } 117 118 if ("CVS".equals(name) || 119 "thumbs.db".equalsIgnoreCase(name) || 120 "picasa.ini".equalsIgnoreCase(name)) { 121 return true; 122 } 123 124 String ext = getExtension(name); 125 if ("scc".equalsIgnoreCase(ext)) { 126 return true; 127 } 128 129 return false; 130 } 131 } 132 133 private final static InputPathFactory sPathFactory = new InputPathFactory() { 134 135 @Override 136 public InputPath createPath(File file, Set<String> extensionsToCheck) { 137 return new ResFolderInputPath(file, extensionsToCheck); 138 } 139 }; 140 141 /** 142 * Sets the value of the "executable" attribute. 143 * @param executable the value. 144 */ 145 public void setExecutable(Path executable) { 146 mExecutable = TaskHelper.checkSinglePath("executable", executable); 147 } 148 149 /** 150 * Sets the value of the "command" attribute. 151 * @param command the value. 152 */ 153 public void setCommand(String command) { 154 mCommand = command; 155 } 156 157 /** 158 * Sets the value of the "force" attribute. 159 * @param force the value. 160 */ 161 public void setForce(boolean force) { 162 mForce = force; 163 } 164 165 /** 166 * Sets the value of the "verbose" attribute. 167 * @param verbose the value. 168 */ 169 public void setVerbose(boolean verbose) { 170 mVerbose = verbose; 171 } 172 173 /** 174 * Sets the value of the "usecrunchcache" attribute 175 * @param usecrunch whether to use the crunch cache. 176 */ 177 public void setNoCrunch(boolean nocrunch) { 178 mUseCrunchCache = nocrunch; 179 } 180 181 public void setNonConstantId(boolean nonConstantId) { 182 mNonConstantId = nonConstantId; 183 } 184 185 public void setVersioncode(String versionCode) { 186 if (versionCode.length() > 0) { 187 try { 188 mVersionCode = Integer.decode(versionCode); 189 } catch (NumberFormatException e) { 190 System.out.println(String.format( 191 "WARNING: Ignoring invalid version code value '%s'.", versionCode)); 192 } 193 } 194 } 195 196 /** 197 * Sets the value of the "versionName" attribute 198 * @param versionName the value 199 */ 200 public void setVersionname(String versionName) { 201 mVersionName = versionName; 202 } 203 204 public void setDebug(boolean value) { 205 mDebug = value; 206 } 207 208 /** 209 * Sets the value of the "manifest" attribute. 210 * @param manifest the value. 211 */ 212 public void setManifest(Path manifest) { 213 mManifest = TaskHelper.checkSinglePath("manifest", manifest); 214 } 215 216 /** 217 * Sets a custom manifest package ID to be used during packaging.<p> 218 * The manifest will be rewritten so that its package ID becomes the value given here. 219 * Relative class names in the manifest (e.g. ".Foo") will be rewritten to absolute names based 220 * on the existing package name, meaning that no code changes need to be made. 221 * 222 * @param packageName The package ID the APK should have. 223 */ 224 public void setManifestpackage(String packageName) { 225 if (packageName != null && packageName.length() != 0) { 226 mManifestPackage = packageName; 227 } 228 } 229 230 /** 231 * Sets the value of the "resources" attribute. 232 * @param resources the value. 233 * 234 * @deprecated Use nested element(s) <res path="value" /> 235 */ 236 @Deprecated 237 public void setResources(Path resources) { 238 System.out.println("WARNNG: Using deprecated 'resources' attribute in AaptExecLoopTask." + 239 "Use nested element(s) <res path=\"value\" /> instead."); 240 if (mResources == null) { 241 mResources = new ArrayList<Path>(); 242 } 243 244 mResources.add(new Path(getProject(), resources.toString())); 245 } 246 247 /** 248 * Sets the value of the "assets" attribute. 249 * @param assets the value. 250 */ 251 public void setAssets(Path assets) { 252 mAssets = TaskHelper.checkSinglePath("assets", assets); 253 } 254 255 /** 256 * Sets the value of the "androidjar" attribute. 257 * @param androidJar the value. 258 */ 259 public void setAndroidjar(Path androidJar) { 260 mAndroidJar = TaskHelper.checkSinglePath("androidjar", androidJar); 261 } 262 263 /** 264 * Sets the value of the "outfolder" attribute. 265 * @param outFolder the value. 266 * @deprecated use {@link #setApkfolder(Path)} 267 */ 268 @Deprecated 269 public void setOutfolder(Path outFolder) { 270 System.out.println("WARNNG: Using deprecated 'outfolder' attribute in AaptExecLoopTask." + 271 "Use 'apkfolder' (path) instead."); 272 mApkFolder = TaskHelper.checkSinglePath("outfolder", outFolder); 273 } 274 275 /** 276 * Sets the value of the "apkfolder" attribute. 277 * @param apkFolder the value. 278 */ 279 public void setApkfolder(Path apkFolder) { 280 mApkFolder = TaskHelper.checkSinglePath("apkfolder", apkFolder); 281 } 282 283 /** 284 * Sets the value of the resourcefilename attribute 285 * @param apkName the value 286 */ 287 public void setResourcefilename(String apkName) { 288 mApkName = apkName; 289 } 290 291 /** 292 * Sets the value of the "rfolder" attribute. 293 * @param rFolder the value. 294 */ 295 public void setRfolder(Path rFolder) { 296 mRFolder = TaskHelper.checkSinglePath("rfolder", rFolder); 297 } 298 299 public void setresourcefilter(String filter) { 300 if (filter != null && filter.length() > 0) { 301 mResourceFilter = filter; 302 } 303 } 304 305 /** 306 * Set the property name of the property that contains the list of res folder for 307 * Library Projects. This sets the name and not the value itself to handle the case where 308 * it doesn't exist. 309 * @param projectLibrariesResName 310 */ 311 public void setLibraryResFolderPathRefid(String libraryResFolderPathRefid) { 312 mLibraryResFolderPathRefid = libraryResFolderPathRefid; 313 } 314 315 public void setLibraryPackagesRefid(String libraryPackagesRefid) { 316 mLibraryPackagesRefid = libraryPackagesRefid; 317 } 318 319 /** 320 * Returns an object representing a nested <var>nocompress</var> element. 321 */ 322 public Object createNocompress() { 323 NoCompress nc = new NoCompress(); 324 mNoCompressList.add(nc); 325 return nc; 326 } 327 328 /** 329 * Returns an object representing a nested <var>res</var> element. 330 */ 331 public Object createRes() { 332 if (mResources == null) { 333 mResources = new ArrayList<Path>(); 334 } 335 336 Path path = new Path(getProject()); 337 mResources.add(path); 338 339 return path; 340 } 341 342 /* 343 * (non-Javadoc) 344 * 345 * Executes the loop. Based on the values inside project.properties, this will 346 * create alternate temporary ap_ files. 347 * 348 * @see org.apache.tools.ant.Task#execute() 349 */ 350 @Override 351 public void execute() throws BuildException { 352 if (mLibraryResFolderPathRefid == null) { 353 throw new BuildException("Missing attribute libraryResFolderPathRefid"); 354 } 355 if (mLibraryPackagesRefid == null) { 356 throw new BuildException("Missing attribute libraryPackagesRefid"); 357 } 358 359 Project taskProject = getProject(); 360 361 String libPkgProp = null; 362 363 // if the parameters indicate generation of the R class, check if 364 // more R classes need to be created for libraries, only if this project itself 365 // is not a library 366 if (mNonConstantId == false && mRFolder != null && new File(mRFolder).isDirectory()) { 367 libPkgProp = taskProject.getProperty(mLibraryPackagesRefid); 368 if (libPkgProp != null) { 369 // Replace ";" with ":" since that's what aapt expects 370 libPkgProp = libPkgProp.replace(';', ':'); 371 } 372 } 373 // Call aapt. If there are libraries, we'll pass a non-null string of libs. 374 callAapt(libPkgProp); 375 } 376 377 @Override 378 protected String getExecTaskName() { 379 return "aapt"; 380 } 381 382 /** 383 * Calls aapt with the given parameters. 384 * @param resourceFilter the resource configuration filter to pass to aapt (if configName is 385 * non null) 386 * @param extraPackages an optional list of colon-separated packages. Can be null 387 * Ex: com.foo.one:com.foo.two:com.foo.lib 388 */ 389 private void callAapt(String extraPackages) { 390 Project taskProject = getProject(); 391 392 final boolean generateRClass = mRFolder != null && new File(mRFolder).isDirectory(); 393 394 // Get whether we have libraries 395 Object libResRef = taskProject.getReference(mLibraryResFolderPathRefid); 396 397 // Set up our input paths that matter for dependency checks 398 ArrayList<File> paths = new ArrayList<File>(); 399 400 // the project res folder is an input path of course 401 for (Path pathList : mResources) { 402 for (String path : pathList.list()) { 403 paths.add(new File(path)); 404 } 405 } 406 407 // and if libraries exist, their res folders folders too. 408 if (libResRef instanceof Path) { 409 for (String path : ((Path)libResRef).list()) { 410 paths.add(new File(path)); 411 } 412 } 413 414 // Now we figure out what we need to do 415 if (generateRClass) { 416 // in this case we only want to run aapt if an XML file was touched, or if any 417 // file is added/removed 418 List<InputPath> inputPaths = getInputPaths(paths, Collections.singleton("xml"), 419 sPathFactory); 420 421 // let's not forget the manifest as an input path (with no extension restrictions). 422 if (mManifest != null) { 423 inputPaths.add(new InputPath(new File(mManifest))); 424 } 425 426 // Check to see if our dependencies have changed. If not, then skip 427 if (initDependencies(mRFolder + File.separator + "R.java.d", inputPaths) 428 && dependenciesHaveChanged() == false) { 429 System.out.println("No changed resources. R.java and Manifest.java untouched."); 430 return; 431 } else { 432 System.out.println("Generating resource IDs..."); 433 } 434 } else { 435 // in this case we want to run aapt if any file was updated/removed/added in any of the 436 // input paths 437 List<InputPath> inputPaths = getInputPaths(paths, null /*extensionsToCheck*/, 438 sPathFactory); 439 440 // let's not forget the manifest as an input path. 441 if (mManifest != null) { 442 inputPaths.add(new InputPath(new File(mManifest))); 443 } 444 445 // If we're here to generate a .ap_ file we need to use assets as an input path as well. 446 if (mAssets != null) { 447 File assetsDir = new File(mAssets); 448 if (assetsDir.isDirectory()) { 449 inputPaths.add(new InputPath(assetsDir)); 450 } 451 } 452 453 // Find our dependency file. It should have the same name as our target .ap_ but 454 // with a .d extension 455 String dependencyFilePath = mApkFolder + File.separator + mApkName; 456 dependencyFilePath += ".d"; 457 458 // Check to see if our dependencies have changed 459 if (initDependencies(dependencyFilePath, inputPaths) 460 && dependenciesHaveChanged() == false) { 461 System.out.println("No changed resources or assets. " + mApkName 462 + " remains untouched"); 463 return; 464 } 465 if (mResourceFilter == null) { 466 System.out.println("Creating full resource package..."); 467 } else { 468 System.out.println(String.format( 469 "Creating resource package with filter: (%1$s)...", 470 mResourceFilter)); 471 } 472 } 473 474 // create a task for the default apk. 475 ExecTask task = new ExecTask(); 476 task.setExecutable(mExecutable); 477 task.setFailonerror(true); 478 479 task.setTaskName(getExecTaskName()); 480 481 // aapt command. Only "package" is supported at this time really. 482 task.createArg().setValue(mCommand); 483 484 // No crunch flag 485 if (mUseCrunchCache) { 486 task.createArg().setValue("--no-crunch"); 487 } 488 489 if (mNonConstantId) { 490 task.createArg().setValue("--non-constant-id"); 491 } 492 493 // force flag 494 if (mForce) { 495 task.createArg().setValue("-f"); 496 } 497 498 // verbose flag 499 if (mVerbose) { 500 task.createArg().setValue("-v"); 501 } 502 503 if (mDebug) { 504 task.createArg().setValue("--debug-mode"); 505 } 506 507 if (generateRClass) { 508 task.createArg().setValue("-m"); 509 } 510 511 // filters if needed 512 if (mResourceFilter != null && mResourceFilter.length() > 0) { 513 task.createArg().setValue("-c"); 514 task.createArg().setValue(mResourceFilter); 515 } 516 517 // no compress flag 518 // first look to see if there's a NoCompress object with no specified extension 519 boolean compressNothing = false; 520 for (NoCompress nc : mNoCompressList) { 521 if (nc.mExtension == null) { 522 task.createArg().setValue("-0"); 523 task.createArg().setValue(""); 524 compressNothing = true; 525 break; 526 } 527 } 528 529 if (compressNothing == false) { 530 for (NoCompress nc : mNoCompressList) { 531 task.createArg().setValue("-0"); 532 task.createArg().setValue(nc.mExtension); 533 } 534 } 535 536 if (mNonConstantId == false && extraPackages != null && extraPackages.length() > 0) { 537 task.createArg().setValue("--extra-packages"); 538 task.createArg().setValue(extraPackages); 539 } 540 541 // if the project contains libraries, force auto-add-overlay 542 if (libResRef != null) { 543 task.createArg().setValue("--auto-add-overlay"); 544 } 545 546 if (mVersionCode != 0) { 547 task.createArg().setValue("--version-code"); 548 task.createArg().setValue(Integer.toString(mVersionCode)); 549 } 550 551 if (mVersionName != null && mVersionName.length() > 0) { 552 task.createArg().setValue("--version-name"); 553 task.createArg().setValue(mVersionName); 554 } 555 556 // manifest location 557 if (mManifest != null && mManifest.length() > 0) { 558 task.createArg().setValue("-M"); 559 task.createArg().setValue(mManifest); 560 } 561 562 // Rename manifest package 563 if (mManifestPackage != null) { 564 task.createArg().setValue("--rename-manifest-package"); 565 task.createArg().setValue(mManifestPackage); 566 } 567 568 // resources locations. 569 if (mResources.size() > 0) { 570 for (Path pathList : mResources) { 571 for (String path : pathList.list()) { 572 // This may not exists, and aapt doesn't like it, so we check first. 573 File res = new File(path); 574 if (res.isDirectory()) { 575 task.createArg().setValue("-S"); 576 task.createArg().setValue(path); 577 } 578 } 579 } 580 } 581 582 // add other resources coming from library project 583 if (libResRef instanceof Path) { 584 for (String path : ((Path)libResRef).list()) { 585 // This may not exists, and aapt doesn't like it, so we check first. 586 File res = new File(path); 587 if (res.isDirectory()) { 588 task.createArg().setValue("-S"); 589 task.createArg().setValue(path); 590 } 591 } 592 } 593 594 // assets location. This may not exists, and aapt doesn't like it, so we check first. 595 if (mAssets != null && new File(mAssets).isDirectory()) { 596 task.createArg().setValue("-A"); 597 task.createArg().setValue(mAssets); 598 } 599 600 // android.jar 601 if (mAndroidJar != null) { 602 task.createArg().setValue("-I"); 603 task.createArg().setValue(mAndroidJar); 604 } 605 606 // apk file. This is based on the apkFolder, apkBaseName, and the configName (if applicable) 607 String filename = null; 608 if (mApkName != null) { 609 filename = mApkName; 610 } 611 612 if (filename != null) { 613 File file = new File(mApkFolder, filename); 614 task.createArg().setValue("-F"); 615 task.createArg().setValue(file.getAbsolutePath()); 616 } 617 618 // R class generation 619 if (generateRClass) { 620 task.createArg().setValue("-J"); 621 task.createArg().setValue(mRFolder); 622 } 623 624 // Use dependency generation 625 task.createArg().setValue("--generate-dependencies"); 626 627 // final setup of the task 628 task.setProject(taskProject); 629 task.setOwningTarget(getOwningTarget()); 630 631 // execute it. 632 task.execute(); 633 } 634 } 635