1 /* 2 * Copyright (C) 2008 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; 18 19 import com.android.prefs.AndroidLocation; 20 import com.android.prefs.AndroidLocation.AndroidLocationException; 21 import com.android.sdklib.AndroidVersion.AndroidVersionException; 22 import com.android.sdklib.internal.project.ProjectProperties; 23 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileNotFoundException; 27 import java.io.FileWriter; 28 import java.io.IOException; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.Map; 34 import java.util.Properties; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 /** 39 * The SDK manager parses the SDK folder and gives access to the content. 40 * @see PlatformTarget 41 * @see AddOnTarget 42 */ 43 public final class SdkManager { 44 45 public final static String PROP_VERSION_SDK = "ro.build.version.sdk"; 46 public final static String PROP_VERSION_CODENAME = "ro.build.version.codename"; 47 public final static String PROP_VERSION_RELEASE = "ro.build.version.release"; 48 49 private final static String ADDON_NAME = "name"; 50 private final static String ADDON_VENDOR = "vendor"; 51 private final static String ADDON_API = "api"; 52 private final static String ADDON_DESCRIPTION = "description"; 53 private final static String ADDON_LIBRARIES = "libraries"; 54 private final static String ADDON_DEFAULT_SKIN = "skin"; 55 private final static String ADDON_USB_VENDOR = "usb-vendor"; 56 private final static String ADDON_REVISION = "revision"; 57 private final static String ADDON_REVISION_OLD = "version"; 58 59 60 private final static Pattern PATTERN_LIB_DATA = Pattern.compile( 61 "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); 62 63 // usb ids are 16-bit hexadecimal values. 64 private final static Pattern PATTERN_USB_IDS = Pattern.compile( 65 "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); 66 67 /** List of items in the platform to check when parsing it. These paths are relative to the 68 * platform root folder. */ 69 private final static String[] sPlatformContentList = new String[] { 70 SdkConstants.FN_FRAMEWORK_LIBRARY, 71 SdkConstants.FN_FRAMEWORK_AIDL, 72 SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT, 73 SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL, 74 SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX, 75 SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR, 76 }; 77 78 /** Preference file containing the usb ids for adb */ 79 private final static String ADB_INI_FILE = "adb_usb.ini"; 80 //0--------90--------90--------90--------90--------90--------90--------90--------9 81 private final static String ADB_INI_HEADER = 82 "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" + 83 "# USE 'android update adb' TO GENERATE.\n" + 84 "# 1 USB VENDOR ID PER LINE.\n"; 85 86 /** the location of the SDK */ 87 private final String mSdkLocation; 88 private IAndroidTarget[] mTargets; 89 90 /** 91 * Create a new {@link SdkManager} instance. 92 * External users should use {@link #createManager(String, ISdkLog)}. 93 * 94 * @param sdkLocation the location of the SDK. 95 */ 96 private SdkManager(String sdkLocation) { 97 mSdkLocation = sdkLocation; 98 } 99 100 /** 101 * Creates an {@link SdkManager} for a given sdk location. 102 * @param sdkLocation the location of the SDK. 103 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 104 * @return the created {@link SdkManager} or null if the location is not valid. 105 */ 106 public static SdkManager createManager(String sdkLocation, ISdkLog log) { 107 try { 108 SdkManager manager = new SdkManager(sdkLocation); 109 ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); 110 loadPlatforms(sdkLocation, list, log); 111 loadAddOns(sdkLocation, list, log); 112 113 // sort the targets/add-ons 114 Collections.sort(list); 115 116 manager.setTargets(list.toArray(new IAndroidTarget[list.size()])); 117 118 // load the samples, after the targets have been set. 119 manager.loadSamples(log); 120 121 return manager; 122 } catch (IllegalArgumentException e) { 123 log.error(e, "Error parsing the sdk."); 124 } 125 126 return null; 127 } 128 129 /** 130 * Returns the location of the SDK. 131 */ 132 public String getLocation() { 133 return mSdkLocation; 134 } 135 136 /** 137 * Returns the targets that are available in the SDK. 138 * <p/> 139 * The array can be empty but not null. 140 */ 141 public IAndroidTarget[] getTargets() { 142 return mTargets; 143 } 144 145 /** 146 * Sets the targets that are available in the SDK. 147 * <p/> 148 * The array can be empty but not null. 149 */ 150 private void setTargets(IAndroidTarget[] targets) { 151 assert targets != null; 152 mTargets = targets; 153 } 154 155 /** 156 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. 157 * 158 * @param hash the {@link IAndroidTarget} hash string. 159 * @return The matching {@link IAndroidTarget} or null. 160 */ 161 public IAndroidTarget getTargetFromHashString(String hash) { 162 if (hash != null) { 163 for (IAndroidTarget target : mTargets) { 164 if (hash.equals(target.hashString())) { 165 return target; 166 } 167 } 168 } 169 170 return null; 171 } 172 173 /** 174 * Updates adb with the USB devices declared in the SDK add-ons. 175 * @throws AndroidLocationException 176 * @throws IOException 177 */ 178 public void updateAdb() throws AndroidLocationException, IOException { 179 FileWriter writer = null; 180 try { 181 // get the android prefs location to know where to write the file. 182 File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE); 183 writer = new FileWriter(adbIni); 184 185 // first, put all the vendor id in an HashSet to remove duplicate. 186 HashSet<Integer> set = new HashSet<Integer>(); 187 IAndroidTarget[] targets = getTargets(); 188 for (IAndroidTarget target : targets) { 189 if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) { 190 set.add(target.getUsbVendorId()); 191 } 192 } 193 194 // write file header. 195 writer.write(ADB_INI_HEADER); 196 197 // now write the Id in a text file, one per line. 198 for (Integer i : set) { 199 writer.write(String.format("0x%04x\n", i)); 200 } 201 } finally { 202 if (writer != null) { 203 writer.close(); 204 } 205 } 206 } 207 208 /** 209 * Reloads the content of the SDK. 210 * 211 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 212 */ 213 public void reloadSdk(ISdkLog log) { 214 // get the current target list. 215 ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); 216 loadPlatforms(mSdkLocation, list, log); 217 loadAddOns(mSdkLocation, list, log); 218 219 // For now replace the old list with the new one. 220 // In the future we may want to keep the current objects, so that ADT doesn't have to deal 221 // with new IAndroidTarget objects when a target didn't actually change. 222 223 // sort the targets/add-ons 224 Collections.sort(list); 225 setTargets(list.toArray(new IAndroidTarget[list.size()])); 226 227 // load the samples, after the targets have been set. 228 loadSamples(log); 229 } 230 231 /** 232 * Loads the Platforms from the SDK. 233 * @param location Location of the SDK 234 * @param list the list to fill with the platforms. 235 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 236 */ 237 private static void loadPlatforms(String location, ArrayList<IAndroidTarget> list, 238 ISdkLog log) { 239 File platformFolder = new File(location, SdkConstants.FD_PLATFORMS); 240 if (platformFolder.isDirectory()) { 241 File[] platforms = platformFolder.listFiles(); 242 243 for (File platform : platforms) { 244 if (platform.isDirectory()) { 245 PlatformTarget target = loadPlatform(platform, log); 246 if (target != null) { 247 list.add(target); 248 } 249 } else { 250 log.warning("Ignoring platform '%1$s', not a folder.", platform.getName()); 251 } 252 } 253 254 return; 255 } 256 257 String message = null; 258 if (platformFolder.exists() == false) { 259 message = "%s is missing."; 260 } else { 261 message = "%s is not a folder."; 262 } 263 264 throw new IllegalArgumentException(String.format(message, 265 platformFolder.getAbsolutePath())); 266 } 267 268 /** 269 * Loads a specific Platform at a given location. 270 * @param platform the location of the platform. 271 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 272 */ 273 private static PlatformTarget loadPlatform(File platform, ISdkLog log) { 274 File buildProp = new File(platform, SdkConstants.FN_BUILD_PROP); 275 276 if (buildProp.isFile()) { 277 Map<String, String> map = ProjectProperties.parsePropertyFile(buildProp, log); 278 279 if (map != null) { 280 // look for some specific values in the map. 281 282 // version string 283 String apiName = map.get(PROP_VERSION_RELEASE); 284 if (apiName == null) { 285 log.warning( 286 "Ignoring platform '%1$s': %2$s is missing from '%3$s'", 287 platform.getName(), PROP_VERSION_RELEASE, 288 SdkConstants.FN_BUILD_PROP); 289 return null; 290 } 291 292 // api level 293 int apiNumber; 294 String stringValue = map.get(PROP_VERSION_SDK); 295 if (stringValue == null) { 296 log.warning( 297 "Ignoring platform '%1$s': %2$s is missing from '%3$s'", 298 platform.getName(), PROP_VERSION_SDK, 299 SdkConstants.FN_BUILD_PROP); 300 return null; 301 } else { 302 try { 303 apiNumber = Integer.parseInt(stringValue); 304 } catch (NumberFormatException e) { 305 // looks like apiNumber does not parse to a number. 306 // Ignore this platform. 307 log.warning( 308 "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.", 309 platform.getName(), PROP_VERSION_SDK, 310 SdkConstants.FN_BUILD_PROP); 311 return null; 312 } 313 } 314 315 // codename (optional) 316 String apiCodename = map.get(PROP_VERSION_CODENAME); 317 if (apiCodename != null && apiCodename.equals("REL")) { 318 apiCodename = null; // REL means it's a release version and therefore the 319 // codename is irrelevant at this point. 320 } 321 322 // platform rev number 323 int revision = 1; 324 File sourcePropFile = new File(platform, SdkConstants.FN_SOURCE_PROP); 325 Map<String, String> sourceProp = ProjectProperties.parsePropertyFile(sourcePropFile, 326 log); 327 if (sourceProp != null) { 328 try { 329 revision = Integer.parseInt(sourceProp.get("Pkg.Revision")); 330 } catch (NumberFormatException e) { 331 // do nothing, we'll keep the default value of 1. 332 } 333 map.putAll(sourceProp); 334 } 335 336 // Ant properties 337 File sdkPropFile = new File(platform, SdkConstants.FN_SDK_PROP); 338 Map<String, String> antProp = ProjectProperties.parsePropertyFile(sdkPropFile, log); 339 if (antProp != null) { 340 map.putAll(antProp); 341 } 342 343 // api number and name look valid, perform a few more checks 344 if (checkPlatformContent(platform, log) == false) { 345 return null; 346 } 347 // create the target. 348 PlatformTarget target = new PlatformTarget( 349 platform.getAbsolutePath(), 350 map, 351 apiNumber, 352 apiCodename, 353 apiName, 354 revision); 355 356 // need to parse the skins. 357 String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); 358 target.setSkins(skins); 359 360 return target; 361 } 362 } else { 363 log.warning("Ignoring platform '%1$s': %2$s is missing.", platform.getName(), 364 SdkConstants.FN_BUILD_PROP); 365 } 366 367 return null; 368 } 369 370 371 /** 372 * Loads the Add-on from the SDK. 373 * @param location Location of the SDK 374 * @param list the list to fill with the add-ons. 375 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 376 */ 377 private static void loadAddOns(String location, ArrayList<IAndroidTarget> list, ISdkLog log) { 378 File addonFolder = new File(location, SdkConstants.FD_ADDONS); 379 if (addonFolder.isDirectory()) { 380 File[] addons = addonFolder.listFiles(); 381 382 for (File addon : addons) { 383 // Add-ons have to be folders. Ignore files and no need to warn about them. 384 if (addon.isDirectory()) { 385 AddOnTarget target = loadAddon(addon, list, log); 386 if (target != null) { 387 list.add(target); 388 } 389 } 390 } 391 392 return; 393 } 394 395 String message = null; 396 if (addonFolder.exists() == false) { 397 message = "%s is missing."; 398 } else { 399 message = "%s is not a folder."; 400 } 401 402 throw new IllegalArgumentException(String.format(message, 403 addonFolder.getAbsolutePath())); 404 } 405 406 /** 407 * Loads a specific Add-on at a given location. 408 * @param addon the location of the addon. 409 * @param targetList The list of Android target that were already loaded from the SDK. 410 * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. 411 */ 412 private static AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> targetList, 413 ISdkLog log) { 414 File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI); 415 416 if (addOnManifest.isFile()) { 417 Map<String, String> propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, 418 log); 419 420 if (propertyMap != null) { 421 // look for some specific values in the map. 422 // we require name, vendor, and api 423 String name = propertyMap.get(ADDON_NAME); 424 if (name == null) { 425 displayAddonManifestWarning(log, addon.getName(), ADDON_NAME); 426 return null; 427 } 428 429 String vendor = propertyMap.get(ADDON_VENDOR); 430 if (vendor == null) { 431 displayAddonManifestWarning(log, addon.getName(), ADDON_VENDOR); 432 return null; 433 } 434 435 String api = propertyMap.get(ADDON_API); 436 PlatformTarget baseTarget = null; 437 if (api == null) { 438 displayAddonManifestWarning(log, addon.getName(), ADDON_API); 439 return null; 440 } else { 441 // Look for a platform that has a matching api level or codename. 442 for (IAndroidTarget target : targetList) { 443 if (target.isPlatform() && target.getVersion().equals(api)) { 444 baseTarget = (PlatformTarget)target; 445 break; 446 } 447 } 448 449 if (baseTarget == null) { 450 // Ignore this add-on. 451 log.warning( 452 "Ignoring add-on '%1$s': Unable to find base platform with API level '%2$s'", 453 addon.getName(), api); 454 return null; 455 } 456 } 457 458 // get the optional description 459 String description = propertyMap.get(ADDON_DESCRIPTION); 460 461 // get the add-on revision 462 int revisionValue = 1; 463 String revision = propertyMap.get(ADDON_REVISION); 464 if (revision == null) { 465 revision = propertyMap.get(ADDON_REVISION_OLD); 466 } 467 if (revision != null) { 468 try { 469 revisionValue = Integer.parseInt(revision); 470 } catch (NumberFormatException e) { 471 // looks like apiNumber does not parse to a number. 472 // Ignore this add-on. 473 log.warning( 474 "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.", 475 addon.getName(), ADDON_REVISION, SdkConstants.FN_BUILD_PROP); 476 return null; 477 } 478 } 479 480 // get the optional libraries 481 String librariesValue = propertyMap.get(ADDON_LIBRARIES); 482 Map<String, String[]> libMap = null; 483 484 if (librariesValue != null) { 485 librariesValue = librariesValue.trim(); 486 if (librariesValue.length() > 0) { 487 // split in the string into the libraries name 488 String[] libraries = librariesValue.split(";"); 489 if (libraries.length > 0) { 490 libMap = new HashMap<String, String[]>(); 491 for (String libName : libraries) { 492 libName = libName.trim(); 493 494 // get the library data from the properties 495 String libData = propertyMap.get(libName); 496 497 if (libData != null) { 498 // split the jar file from the description 499 Matcher m = PATTERN_LIB_DATA.matcher(libData); 500 if (m.matches()) { 501 libMap.put(libName, new String[] { 502 m.group(1), m.group(2) }); 503 } else { 504 log.warning( 505 "Ignoring library '%1$s', property value has wrong format\n\t%2$s", 506 libName, libData); 507 } 508 } else { 509 log.warning( 510 "Ignoring library '%1$s', missing property value", 511 libName, libData); 512 } 513 } 514 } 515 } 516 } 517 518 AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor, 519 revisionValue, description, libMap, baseTarget); 520 521 // need to parse the skins. 522 String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); 523 524 // get the default skin, or take it from the base platform if needed. 525 String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN); 526 if (defaultSkin == null) { 527 if (skins.length == 1) { 528 defaultSkin = skins[0]; 529 } else { 530 defaultSkin = baseTarget.getDefaultSkin(); 531 } 532 } 533 534 // get the USB ID (if available) 535 int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR)); 536 if (usbVendorId != IAndroidTarget.NO_USB_ID) { 537 target.setUsbVendorId(usbVendorId); 538 } 539 540 target.setSkins(skins, defaultSkin); 541 542 return target; 543 } 544 } else { 545 log.warning("Ignoring add-on '%1$s': %2$s is missing.", addon.getName(), 546 SdkConstants.FN_MANIFEST_INI); 547 } 548 549 return null; 550 } 551 552 /** 553 * Converts a string representation of an hexadecimal ID into an int. 554 * @param value the string to convert. 555 * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed. 556 */ 557 private static int convertId(String value) { 558 if (value != null && value.length() > 0) { 559 if (PATTERN_USB_IDS.matcher(value).matches()) { 560 String v = value.substring(2); 561 try { 562 return Integer.parseInt(v, 16); 563 } catch (NumberFormatException e) { 564 // this shouldn't happen since we check the pattern above, but this is safer. 565 // the method will return 0 below. 566 } 567 } 568 } 569 570 return IAndroidTarget.NO_USB_ID; 571 } 572 573 /** 574 * Displays a warning in the log about the addon being ignored due to a missing manifest value. 575 * 576 * @param log The logger object. Cannot be null. 577 * @param addonName The addon name, for display. 578 * @param valueName The missing manifest value, for display. 579 */ 580 private static void displayAddonManifestWarning(ISdkLog log, String addonName, String valueName) { 581 log.warning("Ignoring add-on '%1$s': '%2$s' is missing from %3$s.", 582 addonName, valueName, SdkConstants.FN_MANIFEST_INI); 583 } 584 585 /** 586 * Checks the given platform has all the required files, and returns true if they are all 587 * present. 588 * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), 589 * aidl(.exe), dx(.bat), and dx.jar 590 * 591 * @param platform The folder containing the platform. 592 * @param log Logger. Cannot be null. 593 */ 594 private static boolean checkPlatformContent(File platform, ISdkLog log) { 595 for (String relativePath : sPlatformContentList) { 596 File f = new File(platform, relativePath); 597 if (!f.exists()) { 598 log.warning("Ignoring platform '%1$s': %2$s is missing.", 599 platform.getName(), relativePath); 600 return false; 601 } 602 } 603 return true; 604 } 605 606 607 608 /** 609 * Parses the skin folder and builds the skin list. 610 * @param osPath The path of the skin root folder. 611 */ 612 private static String[] parseSkinFolder(String osPath) { 613 File skinRootFolder = new File(osPath); 614 615 if (skinRootFolder.isDirectory()) { 616 ArrayList<String> skinList = new ArrayList<String>(); 617 618 File[] files = skinRootFolder.listFiles(); 619 620 for (File skinFolder : files) { 621 if (skinFolder.isDirectory()) { 622 // check for layout file 623 File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT); 624 625 if (layout.isFile()) { 626 // for now we don't parse the content of the layout and 627 // simply add the directory to the list. 628 skinList.add(skinFolder.getName()); 629 } 630 } 631 } 632 633 return skinList.toArray(new String[skinList.size()]); 634 } 635 636 return new String[0]; 637 } 638 639 /** 640 * Loads all samples from the {@link SdkConstants#FD_SAMPLES} directory. 641 * 642 * @param log Logger. Cannot be null. 643 */ 644 private void loadSamples(ISdkLog log) { 645 File sampleFolder = new File(mSdkLocation, SdkConstants.FD_SAMPLES); 646 if (sampleFolder.isDirectory()) { 647 File[] platforms = sampleFolder.listFiles(); 648 649 for (File platform : platforms) { 650 if (platform.isDirectory()) { 651 // load the source.properties file and get an AndroidVersion object from it. 652 AndroidVersion version = getSamplesVersion(platform, log); 653 654 if (version != null) { 655 // locate the platform matching this version 656 for (IAndroidTarget target : mTargets) { 657 if (target.isPlatform() && target.getVersion().equals(version)) { 658 ((PlatformTarget)target).setSamplesPath(platform.getAbsolutePath()); 659 break; 660 } 661 } 662 } 663 } 664 } 665 } 666 } 667 668 /** 669 * Returns the {@link AndroidVersion} of the sample in the given folder. 670 * 671 * @param folder The sample's folder. 672 * @param log Logger for errors. Cannot be null. 673 * @return An {@link AndroidVersion} or null on error. 674 */ 675 private AndroidVersion getSamplesVersion(File folder, ISdkLog log) { 676 File sourceProp = new File(folder, SdkConstants.FN_SOURCE_PROP); 677 try { 678 Properties p = new Properties(); 679 p.load(new FileInputStream(sourceProp)); 680 681 return new AndroidVersion(p); 682 } catch (FileNotFoundException e) { 683 log.warning("Ignoring sample '%1$s': does not contain %2$s.", //$NON-NLS-1$ 684 folder.getName(), SdkConstants.FN_SOURCE_PROP); 685 } catch (IOException e) { 686 log.warning("Ignoring sample '%1$s': failed reading %2$s.", //$NON-NLS-1$ 687 folder.getName(), SdkConstants.FN_SOURCE_PROP); 688 } catch (AndroidVersionException e) { 689 log.warning("Ignoring sample '%1$s': no android version found in %2$s.", //$NON-NLS-1$ 690 folder.getName(), SdkConstants.FN_SOURCE_PROP); 691 } 692 693 return null; 694 } 695 696 } 697