1 /* 2 * Copyright (C) 2010 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.sdklib.build; 18 19 import com.android.sdklib.SdkConstants; 20 import com.android.sdklib.internal.build.DebugKeyProvider; 21 import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; 22 import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; 23 import com.android.sdklib.internal.build.SignedJarBuilder; 24 import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter; 25 26 import java.io.File; 27 import java.io.FileInputStream; 28 import java.io.FileNotFoundException; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.io.PrintStream; 32 import java.security.PrivateKey; 33 import java.security.cert.X509Certificate; 34 import java.text.DateFormat; 35 import java.util.ArrayList; 36 import java.util.Date; 37 import java.util.HashMap; 38 import java.util.List; 39 import java.util.regex.Pattern; 40 41 /** 42 * Class making the final apk packaging. 43 * The inputs are: 44 * - packaged resources (output of aapt) 45 * - code file (ouput of dx) 46 * - Java resources coming from the project, its libraries, and its jar files 47 * - Native libraries from the project or its library. 48 * 49 */ 50 public final class ApkBuilder implements IArchiveBuilder { 51 52 private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$", 53 Pattern.CASE_INSENSITIVE); 54 55 /** 56 * A No-op zip filter. It's used to detect conflicts. 57 * 58 */ 59 private final class NullZipFilter implements IZipEntryFilter { 60 private File mInputFile; 61 62 void reset(File inputFile) { 63 mInputFile = inputFile; 64 } 65 66 @Override 67 public boolean checkEntry(String archivePath) throws ZipAbortException { 68 verbosePrintln("=> %s", archivePath); 69 70 File duplicate = checkFileForDuplicate(archivePath); 71 if (duplicate != null) { 72 throw new DuplicateFileException(archivePath, duplicate, mInputFile); 73 } else { 74 mAddedFiles.put(archivePath, mInputFile); 75 } 76 77 return true; 78 } 79 } 80 81 /** 82 * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java 83 * resources, and also record whether the zip file contains native libraries. 84 * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when 85 * we only want the java resources from external jars. 86 */ 87 private final class JavaAndNativeResourceFilter implements IZipEntryFilter { 88 private final List<String> mNativeLibs = new ArrayList<String>(); 89 private boolean mNativeLibsConflict = false; 90 private File mInputFile; 91 92 @Override 93 public boolean checkEntry(String archivePath) throws ZipAbortException { 94 // split the path into segments. 95 String[] segments = archivePath.split("/"); 96 97 // empty path? skip to next entry. 98 if (segments.length == 0) { 99 return false; 100 } 101 102 // Check each folders to make sure they should be included. 103 // Folders like CVS, .svn, etc.. should already have been excluded from the 104 // jar file, but we need to exclude some other folder (like /META-INF) so 105 // we check anyway. 106 for (int i = 0 ; i < segments.length - 1; i++) { 107 if (checkFolderForPackaging(segments[i]) == false) { 108 return false; 109 } 110 } 111 112 // get the file name from the path 113 String fileName = segments[segments.length-1]; 114 115 boolean check = checkFileForPackaging(fileName); 116 117 // only do additional checks if the file passes the default checks. 118 if (check) { 119 verbosePrintln("=> %s", archivePath); 120 121 File duplicate = checkFileForDuplicate(archivePath); 122 if (duplicate != null) { 123 throw new DuplicateFileException(archivePath, duplicate, mInputFile); 124 } else { 125 mAddedFiles.put(archivePath, mInputFile); 126 } 127 128 if (archivePath.endsWith(".so")) { 129 mNativeLibs.add(archivePath); 130 131 // only .so located in lib/ will interfere with the installation 132 if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) { 133 mNativeLibsConflict = true; 134 } 135 } else if (archivePath.endsWith(".jnilib")) { 136 mNativeLibs.add(archivePath); 137 } 138 } 139 140 return check; 141 } 142 143 List<String> getNativeLibs() { 144 return mNativeLibs; 145 } 146 147 boolean getNativeLibsConflict() { 148 return mNativeLibsConflict; 149 } 150 151 void reset(File inputFile) { 152 mInputFile = inputFile; 153 mNativeLibs.clear(); 154 mNativeLibsConflict = false; 155 } 156 } 157 158 private File mApkFile; 159 private File mResFile; 160 private File mDexFile; 161 private PrintStream mVerboseStream; 162 private SignedJarBuilder mBuilder; 163 private boolean mDebugMode = false; 164 private boolean mIsSealed = false; 165 166 private final NullZipFilter mNullFilter = new NullZipFilter(); 167 private final JavaAndNativeResourceFilter mFilter = new JavaAndNativeResourceFilter(); 168 private final HashMap<String, File> mAddedFiles = new HashMap<String, File>(); 169 170 /** 171 * Status for the addition of a jar file resources into the APK. 172 * This indicates possible issues with native library inside the jar file. 173 */ 174 public interface JarStatus { 175 /** 176 * Returns the list of native libraries found in the jar file. 177 */ 178 List<String> getNativeLibs(); 179 180 /** 181 * Returns whether some of those libraries were located in the location that Android 182 * expects its native libraries. 183 */ 184 boolean hasNativeLibsConflicts(); 185 186 } 187 188 /** Internal implementation of {@link JarStatus}. */ 189 private final static class JarStatusImpl implements JarStatus { 190 public final List<String> mLibs; 191 public final boolean mNativeLibsConflict; 192 193 private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) { 194 mLibs = libs; 195 mNativeLibsConflict = nativeLibsConflict; 196 } 197 198 @Override 199 public List<String> getNativeLibs() { 200 return mLibs; 201 } 202 203 @Override 204 public boolean hasNativeLibsConflicts() { 205 return mNativeLibsConflict; 206 } 207 } 208 209 /** 210 * Signing information. 211 * 212 * Both the {@link PrivateKey} and the {@link X509Certificate} are guaranteed to be non-null. 213 * 214 */ 215 public final static class SigningInfo { 216 public final PrivateKey key; 217 public final X509Certificate certificate; 218 219 private SigningInfo(PrivateKey key, X509Certificate certificate) { 220 if (key == null || certificate == null) { 221 throw new IllegalArgumentException("key and certificate cannot be null"); 222 } 223 this.key = key; 224 this.certificate = certificate; 225 } 226 } 227 228 /** 229 * Returns the key and certificate from a given debug store. 230 * 231 * It is expected that the store password is 'android' and the key alias and password are 232 * 'androiddebugkey' and 'android' respectively. 233 * 234 * @param storeOsPath the OS path to the debug store. 235 * @param verboseStream an option {@link PrintStream} to display verbose information 236 * @return they key and certificate in a {@link SigningInfo} object or null. 237 * @throws ApkCreationException 238 */ 239 public static SigningInfo getDebugKey(String storeOsPath, final PrintStream verboseStream) 240 throws ApkCreationException { 241 try { 242 if (storeOsPath != null) { 243 File storeFile = new File(storeOsPath); 244 try { 245 checkInputFile(storeFile); 246 } catch (FileNotFoundException e) { 247 // ignore these since the debug store can be created on the fly anyway. 248 } 249 250 // get the debug key 251 if (verboseStream != null) { 252 verboseStream.println(String.format("Using keystore: %s", storeOsPath)); 253 } 254 255 IKeyGenOutput keygenOutput = null; 256 if (verboseStream != null) { 257 keygenOutput = new IKeyGenOutput() { 258 @Override 259 public void out(String message) { 260 verboseStream.println(message); 261 } 262 263 @Override 264 public void err(String message) { 265 verboseStream.println(message); 266 } 267 }; 268 } 269 270 DebugKeyProvider keyProvider = new DebugKeyProvider( 271 storeOsPath, null /*store type*/, keygenOutput); 272 273 PrivateKey key = keyProvider.getDebugKey(); 274 X509Certificate certificate = (X509Certificate)keyProvider.getCertificate(); 275 276 if (key == null) { 277 throw new ApkCreationException("Unable to get debug signature key"); 278 } 279 280 // compare the certificate expiration date 281 if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { 282 // TODO, regenerate a new one. 283 throw new ApkCreationException("Debug Certificate expired on " + 284 DateFormat.getInstance().format(certificate.getNotAfter())); 285 } 286 287 return new SigningInfo(key, certificate); 288 } else { 289 return null; 290 } 291 } catch (KeytoolException e) { 292 if (e.getJavaHome() == null) { 293 throw new ApkCreationException(e.getMessage() + 294 "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" + 295 "You can also manually execute the following command\n:" + 296 e.getCommandLine(), e); 297 } else { 298 throw new ApkCreationException(e.getMessage() + 299 "\nJAVA_HOME is set to: " + e.getJavaHome() + 300 "\nUpdate it if necessary, or manually execute the following command:\n" + 301 e.getCommandLine(), e); 302 } 303 } catch (ApkCreationException e) { 304 throw e; 305 } catch (Exception e) { 306 throw new ApkCreationException(e); 307 } 308 } 309 310 /** 311 * Creates a new instance. 312 * 313 * This creates a new builder that will create the specified output file, using the two 314 * mandatory given input files. 315 * 316 * An optional debug keystore can be provided. If set, it is expected that the store password 317 * is 'android' and the key alias and password are 'androiddebugkey' and 'android'. 318 * 319 * An optional {@link PrintStream} can also be provided for verbose output. If null, there will 320 * be no output. 321 * 322 * @param apkOsPath the OS path of the file to create. 323 * @param resOsPath the OS path of the packaged resource file. 324 * @param dexOsPath the OS path of the dex file. This can be null for apk with no code. 325 * @param verboseStream the stream to which verbose output should go. If null, verbose mode 326 * is not enabled. 327 * @throws ApkCreationException 328 */ 329 public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath, 330 PrintStream verboseStream) throws ApkCreationException { 331 this(new File(apkOsPath), 332 new File(resOsPath), 333 dexOsPath != null ? new File(dexOsPath) : null, 334 storeOsPath, 335 verboseStream); 336 } 337 338 /** 339 * Creates a new instance. 340 * 341 * This creates a new builder that will create the specified output file, using the two 342 * mandatory given input files. 343 * 344 * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK. 345 * 346 * An optional {@link PrintStream} can also be provided for verbose output. If null, there will 347 * be no output. 348 * 349 * @param apkOsPath the OS path of the file to create. 350 * @param resOsPath the OS path of the packaged resource file. 351 * @param dexOsPath the OS path of the dex file. This can be null for apk with no code. 352 * @param key the private key used to sign the package. Can be null. 353 * @param certificate the certificate used to sign the package. Can be null. 354 * @param verboseStream the stream to which verbose output should go. If null, verbose mode 355 * is not enabled. 356 * @throws ApkCreationException 357 */ 358 public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, PrivateKey key, 359 X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { 360 this(new File(apkOsPath), 361 new File(resOsPath), 362 dexOsPath != null ? new File(dexOsPath) : null, 363 key, certificate, 364 verboseStream); 365 } 366 367 /** 368 * Creates a new instance. 369 * 370 * This creates a new builder that will create the specified output file, using the two 371 * mandatory given input files. 372 * 373 * An optional debug keystore can be provided. If set, it is expected that the store password 374 * is 'android' and the key alias and password are 'androiddebugkey' and 'android'. 375 * 376 * An optional {@link PrintStream} can also be provided for verbose output. If null, there will 377 * be no output. 378 * 379 * @param apkFile the file to create 380 * @param resFile the file representing the packaged resource file. 381 * @param dexFile the file representing the dex file. This can be null for apk with no code. 382 * @param debugStoreOsPath the OS path to the debug keystore, if needed or null. 383 * @param verboseStream the stream to which verbose output should go. If null, verbose mode 384 * is not enabled. 385 * @throws ApkCreationException 386 */ 387 public ApkBuilder(File apkFile, File resFile, File dexFile, String debugStoreOsPath, 388 final PrintStream verboseStream) throws ApkCreationException { 389 390 SigningInfo info = getDebugKey(debugStoreOsPath, verboseStream); 391 if (info != null) { 392 init(apkFile, resFile, dexFile, info.key, info.certificate, verboseStream); 393 } else { 394 init(apkFile, resFile, dexFile, null /*key*/, null/*certificate*/, verboseStream); 395 } 396 } 397 398 /** 399 * Creates a new instance. 400 * 401 * This creates a new builder that will create the specified output file, using the two 402 * mandatory given input files. 403 * 404 * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK. 405 * 406 * An optional {@link PrintStream} can also be provided for verbose output. If null, there will 407 * be no output. 408 * 409 * @param apkFile the file to create 410 * @param resFile the file representing the packaged resource file. 411 * @param dexFile the file representing the dex file. This can be null for apk with no code. 412 * @param key the private key used to sign the package. Can be null. 413 * @param certificate the certificate used to sign the package. Can be null. 414 * @param verboseStream the stream to which verbose output should go. If null, verbose mode 415 * is not enabled. 416 * @throws ApkCreationException 417 */ 418 public ApkBuilder(File apkFile, File resFile, File dexFile, PrivateKey key, 419 X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { 420 init(apkFile, resFile, dexFile, key, certificate, verboseStream); 421 } 422 423 424 /** 425 * Constructor init method. 426 * 427 * @see #ApkBuilder(File, File, File, String, PrintStream) 428 * @see #ApkBuilder(String, String, String, String, PrintStream) 429 * @see #ApkBuilder(File, File, File, PrivateKey, X509Certificate, PrintStream) 430 */ 431 private void init(File apkFile, File resFile, File dexFile, PrivateKey key, 432 X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { 433 434 try { 435 checkOutputFile(mApkFile = apkFile); 436 checkInputFile(mResFile = resFile); 437 if (dexFile != null) { 438 checkInputFile(mDexFile = dexFile); 439 } else { 440 mDexFile = null; 441 } 442 mVerboseStream = verboseStream; 443 444 mBuilder = new SignedJarBuilder( 445 new FileOutputStream(mApkFile, false /* append */), key, 446 certificate); 447 448 verbosePrintln("Packaging %s", mApkFile.getName()); 449 450 // add the resources 451 addZipFile(mResFile); 452 453 // add the class dex file at the root of the apk 454 if (mDexFile != null) { 455 addFile(mDexFile, SdkConstants.FN_APK_CLASSES_DEX); 456 } 457 458 } catch (ApkCreationException e) { 459 mBuilder.cleanUp(); 460 throw e; 461 } catch (Exception e) { 462 mBuilder.cleanUp(); 463 throw new ApkCreationException(e); 464 } 465 } 466 467 /** 468 * Sets the debug mode. In debug mode, when native libraries are present, the packaging 469 * will also include one or more copies of gdbserver in the final APK file. 470 * 471 * These are used for debugging native code, to ensure that gdbserver is accessible to the 472 * application. 473 * 474 * There will be one version of gdbserver for each ABI supported by the application. 475 * 476 * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK. 477 * 478 * @param debugMode the debug mode flag. 479 */ 480 public void setDebugMode(boolean debugMode) { 481 mDebugMode = debugMode; 482 } 483 484 /** 485 * Adds a file to the APK at a given path 486 * @param file the file to add 487 * @param archivePath the path of the file inside the APK archive. 488 * @throws ApkCreationException if an error occurred 489 * @throws SealedApkException if the APK is already sealed. 490 * @throws DuplicateFileException if a file conflicts with another already added to the APK 491 * at the same location inside the APK archive. 492 */ 493 @Override 494 public void addFile(File file, String archivePath) throws ApkCreationException, 495 SealedApkException, DuplicateFileException { 496 if (mIsSealed) { 497 throw new SealedApkException("APK is already sealed"); 498 } 499 500 try { 501 doAddFile(file, archivePath); 502 } catch (DuplicateFileException e) { 503 mBuilder.cleanUp(); 504 throw e; 505 } catch (Exception e) { 506 mBuilder.cleanUp(); 507 throw new ApkCreationException(e, "Failed to add %s", file); 508 } 509 } 510 511 /** 512 * Adds the content from a zip file. 513 * All file keep the same path inside the archive. 514 * @param zipFile the zip File. 515 * @throws ApkCreationException if an error occurred 516 * @throws SealedApkException if the APK is already sealed. 517 * @throws DuplicateFileException if a file conflicts with another already added to the APK 518 * at the same location inside the APK archive. 519 */ 520 public void addZipFile(File zipFile) throws ApkCreationException, SealedApkException, 521 DuplicateFileException { 522 if (mIsSealed) { 523 throw new SealedApkException("APK is already sealed"); 524 } 525 526 try { 527 verbosePrintln("%s:", zipFile); 528 529 // reset the filter with this input. 530 mNullFilter.reset(zipFile); 531 532 // ask the builder to add the content of the file. 533 FileInputStream fis = new FileInputStream(zipFile); 534 mBuilder.writeZip(fis, mNullFilter); 535 } catch (DuplicateFileException e) { 536 mBuilder.cleanUp(); 537 throw e; 538 } catch (Exception e) { 539 mBuilder.cleanUp(); 540 throw new ApkCreationException(e, "Failed to add %s", zipFile); 541 } 542 } 543 544 /** 545 * Adds the resources from a jar file. 546 * @param jarFile the jar File. 547 * @return a {@link JarStatus} object indicating if native libraries where found in 548 * the jar file. 549 * @throws ApkCreationException if an error occurred 550 * @throws SealedApkException if the APK is already sealed. 551 * @throws DuplicateFileException if a file conflicts with another already added to the APK 552 * at the same location inside the APK archive. 553 */ 554 public JarStatus addResourcesFromJar(File jarFile) throws ApkCreationException, 555 SealedApkException, DuplicateFileException { 556 if (mIsSealed) { 557 throw new SealedApkException("APK is already sealed"); 558 } 559 560 try { 561 verbosePrintln("%s:", jarFile); 562 563 // reset the filter with this input. 564 mFilter.reset(jarFile); 565 566 // ask the builder to add the content of the file, filtered to only let through 567 // the java resources. 568 FileInputStream fis = new FileInputStream(jarFile); 569 mBuilder.writeZip(fis, mFilter); 570 571 // check if native libraries were found in the external library. This should 572 // constitutes an error or warning depending on if they are in lib/ 573 return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict()); 574 } catch (DuplicateFileException e) { 575 mBuilder.cleanUp(); 576 throw e; 577 } catch (Exception e) { 578 mBuilder.cleanUp(); 579 throw new ApkCreationException(e, "Failed to add %s", jarFile); 580 } 581 } 582 583 /** 584 * Adds the resources from a source folder. 585 * @param sourceFolder the source folder. 586 * @throws ApkCreationException if an error occurred 587 * @throws SealedApkException if the APK is already sealed. 588 * @throws DuplicateFileException if a file conflicts with another already added to the APK 589 * at the same location inside the APK archive. 590 */ 591 public void addSourceFolder(File sourceFolder) throws ApkCreationException, SealedApkException, 592 DuplicateFileException { 593 if (mIsSealed) { 594 throw new SealedApkException("APK is already sealed"); 595 } 596 597 addSourceFolder(this, sourceFolder); 598 } 599 600 /** 601 * Adds the resources from a source folder to a given {@link IArchiveBuilder} 602 * @param sourceFolder the source folder. 603 * @throws ApkCreationException if an error occurred 604 * @throws SealedApkException if the APK is already sealed. 605 * @throws DuplicateFileException if a file conflicts with another already added to the APK 606 * at the same location inside the APK archive. 607 */ 608 public static void addSourceFolder(IArchiveBuilder builder, File sourceFolder) 609 throws ApkCreationException, DuplicateFileException { 610 if (sourceFolder.isDirectory()) { 611 try { 612 // file is a directory, process its content. 613 File[] files = sourceFolder.listFiles(); 614 for (File file : files) { 615 processFileForResource(builder, file, null); 616 } 617 } catch (DuplicateFileException e) { 618 throw e; 619 } catch (Exception e) { 620 throw new ApkCreationException(e, "Failed to add %s", sourceFolder); 621 } 622 } else { 623 // not a directory? check if it's a file or doesn't exist 624 if (sourceFolder.exists()) { 625 throw new ApkCreationException("%s is not a folder", sourceFolder); 626 } else { 627 throw new ApkCreationException("%s does not exist", sourceFolder); 628 } 629 } 630 } 631 632 /** 633 * Adds the native libraries from the top native folder. 634 * The content of this folder must be the various ABI folders. 635 * 636 * This may or may not copy gdbserver into the apk based on whether the debug mode is set. 637 * 638 * @param nativeFolder the native folder. 639 * 640 * @throws ApkCreationException if an error occurred 641 * @throws SealedApkException if the APK is already sealed. 642 * @throws DuplicateFileException if a file conflicts with another already added to the APK 643 * at the same location inside the APK archive. 644 * 645 * @see #setDebugMode(boolean) 646 */ 647 public void addNativeLibraries(File nativeFolder) 648 throws ApkCreationException, SealedApkException, DuplicateFileException { 649 if (mIsSealed) { 650 throw new SealedApkException("APK is already sealed"); 651 } 652 653 if (nativeFolder.isDirectory() == false) { 654 // not a directory? check if it's a file or doesn't exist 655 if (nativeFolder.exists()) { 656 throw new ApkCreationException("%s is not a folder", nativeFolder); 657 } else { 658 throw new ApkCreationException("%s does not exist", nativeFolder); 659 } 660 } 661 662 File[] abiList = nativeFolder.listFiles(); 663 664 verbosePrintln("Native folder: %s", nativeFolder); 665 666 if (abiList != null) { 667 for (File abi : abiList) { 668 if (abi.isDirectory()) { // ignore files 669 670 File[] libs = abi.listFiles(); 671 if (libs != null) { 672 for (File lib : libs) { 673 // only consider files that are .so or, if in debug mode, that 674 // are gdbserver executables 675 if (lib.isFile() && 676 (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || 677 (mDebugMode && 678 SdkConstants.FN_GDBSERVER.equals( 679 lib.getName())))) { 680 String path = 681 SdkConstants.FD_APK_NATIVE_LIBS + "/" + 682 abi.getName() + "/" + lib.getName(); 683 684 try { 685 doAddFile(lib, path); 686 } catch (IOException e) { 687 mBuilder.cleanUp(); 688 throw new ApkCreationException(e, "Failed to add %s", lib); 689 } 690 } 691 } 692 } 693 } 694 } 695 } 696 } 697 698 public void addNativeLibraries(List<FileEntry> entries) throws SealedApkException, 699 DuplicateFileException, ApkCreationException { 700 if (mIsSealed) { 701 throw new SealedApkException("APK is already sealed"); 702 } 703 704 for (FileEntry entry : entries) { 705 try { 706 doAddFile(entry.mFile, entry.mPath); 707 } catch (IOException e) { 708 mBuilder.cleanUp(); 709 throw new ApkCreationException(e, "Failed to add %s", entry.mFile); 710 } 711 } 712 } 713 714 public static final class FileEntry { 715 public final File mFile; 716 public final String mPath; 717 718 FileEntry(File file, String path) { 719 mFile = file; 720 mPath = path; 721 } 722 } 723 724 public static List<FileEntry> getNativeFiles(File nativeFolder, boolean debugMode) 725 throws ApkCreationException { 726 727 if (nativeFolder.isDirectory() == false) { 728 // not a directory? check if it's a file or doesn't exist 729 if (nativeFolder.exists()) { 730 throw new ApkCreationException("%s is not a folder", nativeFolder); 731 } else { 732 throw new ApkCreationException("%s does not exist", nativeFolder); 733 } 734 } 735 736 List<FileEntry> files = new ArrayList<FileEntry>(); 737 738 File[] abiList = nativeFolder.listFiles(); 739 740 if (abiList != null) { 741 for (File abi : abiList) { 742 if (abi.isDirectory()) { // ignore files 743 744 File[] libs = abi.listFiles(); 745 if (libs != null) { 746 for (File lib : libs) { 747 // only consider files that are .so or, if in debug mode, that 748 // are gdbserver executables 749 if (lib.isFile() && 750 (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || 751 (debugMode && 752 SdkConstants.FN_GDBSERVER.equals( 753 lib.getName())))) { 754 String path = 755 SdkConstants.FD_APK_NATIVE_LIBS + "/" + 756 abi.getName() + "/" + lib.getName(); 757 758 files.add(new FileEntry(lib, path)); 759 } 760 } 761 } 762 } 763 } 764 } 765 766 return files; 767 } 768 769 770 771 /** 772 * Seals the APK, and signs it if necessary. 773 * @throws ApkCreationException 774 * @throws ApkCreationException if an error occurred 775 * @throws SealedApkException if the APK is already sealed. 776 */ 777 public void sealApk() throws ApkCreationException, SealedApkException { 778 if (mIsSealed) { 779 throw new SealedApkException("APK is already sealed"); 780 } 781 782 // close and sign the application package. 783 try { 784 mBuilder.close(); 785 mIsSealed = true; 786 } catch (Exception e) { 787 throw new ApkCreationException(e, "Failed to seal APK"); 788 } finally { 789 mBuilder.cleanUp(); 790 } 791 } 792 793 /** 794 * Output a given message if the verbose mode is enabled. 795 * @param format the format string for {@link String#format(String, Object...)} 796 * @param args the string arguments 797 */ 798 private void verbosePrintln(String format, Object... args) { 799 if (mVerboseStream != null) { 800 mVerboseStream.println(String.format(format, args)); 801 } 802 } 803 804 private void doAddFile(File file, String archivePath) throws DuplicateFileException, 805 IOException { 806 verbosePrintln("%1$s => %2$s", file, archivePath); 807 808 File duplicate = checkFileForDuplicate(archivePath); 809 if (duplicate != null) { 810 throw new DuplicateFileException(archivePath, duplicate, file); 811 } 812 813 mAddedFiles.put(archivePath, file); 814 mBuilder.writeFile(file, archivePath); 815 } 816 817 /** 818 * Processes a {@link File} that could be an APK {@link File}, or a folder containing 819 * java resources. 820 * 821 * @param file the {@link File} to process. 822 * @param path the relative path of this file to the source folder. 823 * Can be <code>null</code> to identify a root file. 824 * @throws IOException 825 * @throws DuplicateFileException if a file conflicts with another already added 826 * to the APK at the same location inside the APK archive. 827 * @throws SealedApkException if the APK is already sealed. 828 * @throws ApkCreationException if an error occurred 829 */ 830 private static void processFileForResource(IArchiveBuilder builder, File file, String path) 831 throws IOException, DuplicateFileException, ApkCreationException, SealedApkException { 832 if (file.isDirectory()) { 833 // a directory? we check it 834 if (checkFolderForPackaging(file.getName())) { 835 // if it's valid, we append its name to the current path. 836 if (path == null) { 837 path = file.getName(); 838 } else { 839 path = path + "/" + file.getName(); 840 } 841 842 // and process its content. 843 File[] files = file.listFiles(); 844 for (File contentFile : files) { 845 processFileForResource(builder, contentFile, path); 846 } 847 } 848 } else { 849 // a file? we check it to make sure it should be added 850 if (checkFileForPackaging(file.getName())) { 851 // we append its name to the current path 852 if (path == null) { 853 path = file.getName(); 854 } else { 855 path = path + "/" + file.getName(); 856 } 857 858 // and add it to the apk 859 builder.addFile(file, path); 860 } 861 } 862 } 863 864 /** 865 * Checks if the given path in the APK archive has not already been used and if it has been, 866 * then returns a {@link File} object for the source of the duplicate 867 * @param archivePath the archive path to test. 868 * @return A File object of either a file at the same location or an archive that contains a 869 * file that was put at the same location. 870 */ 871 private File checkFileForDuplicate(String archivePath) { 872 return mAddedFiles.get(archivePath); 873 } 874 875 /** 876 * Checks an output {@link File} object. 877 * This checks the following: 878 * - the file is not an existing directory. 879 * - if the file exists, that it can be modified. 880 * - if it doesn't exists, that a new file can be created. 881 * @param file the File to check 882 * @throws ApkCreationException If the check fails 883 */ 884 private void checkOutputFile(File file) throws ApkCreationException { 885 if (file.isDirectory()) { 886 throw new ApkCreationException("%s is a directory!", file); 887 } 888 889 if (file.exists()) { // will be a file in this case. 890 if (file.canWrite() == false) { 891 throw new ApkCreationException("Cannot write %s", file); 892 } 893 } else { 894 try { 895 if (file.createNewFile() == false) { 896 throw new ApkCreationException("Failed to create %s", file); 897 } 898 } catch (IOException e) { 899 throw new ApkCreationException( 900 "Failed to create '%1$ss': %2$s", file, e.getMessage()); 901 } 902 } 903 } 904 905 /** 906 * Checks an input {@link File} object. 907 * This checks the following: 908 * - the file is not an existing directory. 909 * - that the file exists (if <var>throwIfDoesntExist</var> is <code>false</code>) and can 910 * be read. 911 * @param file the File to check 912 * @throws FileNotFoundException if the file is not here. 913 * @throws ApkCreationException If the file is a folder or a file that cannot be read. 914 */ 915 private static void checkInputFile(File file) throws FileNotFoundException, ApkCreationException { 916 if (file.isDirectory()) { 917 throw new ApkCreationException("%s is a directory!", file); 918 } 919 920 if (file.exists()) { 921 if (file.canRead() == false) { 922 throw new ApkCreationException("Cannot read %s", file); 923 } 924 } else { 925 throw new FileNotFoundException(String.format("%s does not exist", file)); 926 } 927 } 928 929 public static String getDebugKeystore() throws ApkCreationException { 930 try { 931 return DebugKeyProvider.getDefaultKeyStoreOsPath(); 932 } catch (Exception e) { 933 throw new ApkCreationException(e, e.getMessage()); 934 } 935 } 936 937 /** 938 * Checks whether a folder and its content is valid for packaging into the .apk as 939 * standard Java resource. 940 * @param folderName the name of the folder. 941 */ 942 public static boolean checkFolderForPackaging(String folderName) { 943 return folderName.equalsIgnoreCase("CVS") == false && 944 folderName.equalsIgnoreCase(".svn") == false && 945 folderName.equalsIgnoreCase("SCCS") == false && 946 folderName.equalsIgnoreCase("META-INF") == false && 947 folderName.startsWith("_") == false; 948 } 949 950 /** 951 * Checks a file to make sure it should be packaged as standard resources. 952 * @param fileName the name of the file (including extension) 953 * @return true if the file should be packaged as standard java resources. 954 */ 955 public static boolean checkFileForPackaging(String fileName) { 956 String[] fileSegments = fileName.split("\\."); 957 String fileExt = ""; 958 if (fileSegments.length > 1) { 959 fileExt = fileSegments[fileSegments.length-1]; 960 } 961 962 return checkFileForPackaging(fileName, fileExt); 963 } 964 965 /** 966 * Checks a file to make sure it should be packaged as standard resources. 967 * @param fileName the name of the file (including extension) 968 * @param extension the extension of the file (excluding '.') 969 * @return true if the file should be packaged as standard java resources. 970 */ 971 public static boolean checkFileForPackaging(String fileName, String extension) { 972 // ignore hidden files and backup files 973 if (fileName.charAt(0) == '.' || fileName.charAt(fileName.length()-1) == '~') { 974 return false; 975 } 976 977 return "aidl".equalsIgnoreCase(extension) == false && // Aidl files 978 "rs".equalsIgnoreCase(extension) == false && // RenderScript files 979 "rsh".equalsIgnoreCase(extension) == false && // RenderScript header files 980 "d".equalsIgnoreCase(extension) == false && // Dependency files 981 "java".equalsIgnoreCase(extension) == false && // Java files 982 "scala".equalsIgnoreCase(extension) == false && // Scala files 983 "class".equalsIgnoreCase(extension) == false && // Java class files 984 "scc".equalsIgnoreCase(extension) == false && // VisualSourceSafe 985 "swp".equalsIgnoreCase(extension) == false && // vi swap file 986 "thumbs.db".equalsIgnoreCase(fileName) == false && // image index file 987 "picasa.ini".equalsIgnoreCase(fileName) == false && // image index file 988 "package.html".equalsIgnoreCase(fileName) == false && // Javadoc 989 "overview.html".equalsIgnoreCase(fileName) == false; // Javadoc 990 } 991 } 992