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