/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdklib; import com.android.prefs.AndroidLocation; import com.android.prefs.AndroidLocation.AndroidLocationException; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The SDK manager parses the SDK folder and gives access to the content. * @see PlatformTarget * @see AddOnTarget */ public final class SdkManager { public final static String PROP_VERSION_SDK = "ro.build.version.sdk"; public final static String PROP_VERSION_RELEASE = "ro.build.version.release"; private final static String ADDON_NAME = "name"; private final static String ADDON_VENDOR = "vendor"; private final static String ADDON_API = "api"; private final static String ADDON_DESCRIPTION = "description"; private final static String ADDON_LIBRARIES = "libraries"; private final static String ADDON_DEFAULT_SKIN = "skin"; private final static String ADDON_USB_VENDOR = "usb-vendor"; private final static Pattern PATTERN_PROP = Pattern.compile( "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); private final static Pattern PATTERN_LIB_DATA = Pattern.compile( "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); // usb ids are 16-bit hexadecimal values. private final static Pattern PATTERN_USB_IDS = Pattern.compile( "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); /** List of items in the platform to check when parsing it. These paths are relative to the * platform root folder. */ private final static String[] sPlatformContentList = new String[] { SdkConstants.FN_FRAMEWORK_LIBRARY, SdkConstants.FN_FRAMEWORK_AIDL, SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT, SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL, SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR, }; /** Preference file containing the usb ids for adb */ private final static String ADB_INI_FILE = "adb_usb.ini"; //0--------90--------90--------90--------90--------90--------90--------90--------9 private final static String ADB_INI_HEADER = "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" + "# USE 'android update adb' TO GENERATE.\n" + "# 1 USB VENDOR ID PER LINE.\n"; /** the location of the SDK */ private final String mSdkLocation; private IAndroidTarget[] mTargets; /** * Creates an {@link SdkManager} for a given sdk location. * @param sdkLocation the location of the SDK. * @param log the ISdkLog object receiving warning/error from the parsing. * @return the created {@link SdkManager} or null if the location is not valid. */ public static SdkManager createManager(String sdkLocation, ISdkLog log) { try { SdkManager manager = new SdkManager(sdkLocation); ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); manager.loadPlatforms(list, log); manager.loadAddOns(list, log); // sort the targets/add-ons Collections.sort(list); manager.setTargets(list.toArray(new IAndroidTarget[list.size()])); return manager; } catch (IllegalArgumentException e) { if (log != null) { log.error(e, "Error parsing the sdk."); } } return null; } /** * Returns the location of the SDK. */ public String getLocation() { return mSdkLocation; } /** * Returns the targets that are available in the SDK. */ public IAndroidTarget[] getTargets() { return mTargets; } /** * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. * * @param hash the {@link IAndroidTarget} hash string. * @return The matching {@link IAndroidTarget} or null. */ public IAndroidTarget getTargetFromHashString(String hash) { if (hash != null) { for (IAndroidTarget target : mTargets) { if (hash.equals(target.hashString())) { return target; } } } return null; } /** * Updates adb with the USB devices declared in the SDK add-ons. * @throws AndroidLocationException * @throws IOException */ public void updateAdb() throws AndroidLocationException, IOException { FileWriter writer = null; try { // get the android prefs location to know where to write the file. File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE); writer = new FileWriter(adbIni); // first, put all the vendor id in an HashSet to remove duplicate. HashSet<Integer> set = new HashSet<Integer>(); IAndroidTarget[] targets = getTargets(); for (IAndroidTarget target : targets) { if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) { set.add(target.getUsbVendorId()); } } // write file header. writer.write(ADB_INI_HEADER); // now write the Id in a text file, one per line. for (Integer i : set) { writer.write(String.format("0x%04x\n", i)); } } finally { writer.close(); } } private SdkManager(String sdkLocation) { mSdkLocation = sdkLocation; } private void setTargets(IAndroidTarget[] targets) { mTargets = targets; } /** * Loads the Platforms from the SDK. * @param list the list to fill with the platforms. * @param log the ISdkLog object receiving warning/error from the parsing. */ private void loadPlatforms(ArrayList<IAndroidTarget> list, ISdkLog log) { File platformFolder = new File(mSdkLocation, SdkConstants.FD_PLATFORMS); if (platformFolder.isDirectory()) { File[] platforms = platformFolder.listFiles(); for (File platform : platforms) { if (platform.isDirectory()) { PlatformTarget target = loadPlatform(platform, log); if (target != null) { list.add(target); } } else if (log != null) { log.warning("Ignoring platform '%1$s', not a folder.", platform.getName()); } } return; } String message = null; if (platformFolder.exists() == false) { message = "%s is missing."; } else { message = "%s is not a folder."; } throw new IllegalArgumentException(String.format(message, platformFolder.getAbsolutePath())); } /** * Loads a specific Platform at a given location. * @param platform the location of the platform. * @param log the ISdkLog object receiving warning/error from the parsing. */ private PlatformTarget loadPlatform(File platform, ISdkLog log) { File buildProp = new File(platform, SdkConstants.FN_BUILD_PROP); if (buildProp.isFile()) { Map<String, String> map = parsePropertyFile(buildProp, log); if (map != null) { // look for some specific values in the map. try { String apiNumber = map.get(PROP_VERSION_SDK); String apiName = map.get(PROP_VERSION_RELEASE); if (apiNumber != null && apiName != null) { // api number and name looks valid, perform a few more checks if (checkPlatformContent(platform, log) == false) { return null; } // create the target. PlatformTarget target = new PlatformTarget( platform.getAbsolutePath(), map, Integer.parseInt(apiNumber), apiName); // need to parse the skins. String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); target.setSkins(skins); return target; } } catch (NumberFormatException e) { // looks like apiNumber does not parse to a number. // Ignore this platform. if (log != null) { log.error(null, "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.", platform.getName(), PROP_VERSION_SDK, SdkConstants.FN_BUILD_PROP); } } } } else if (log != null) { log.error(null, "Ignoring platform '%1$s': %2$s is missing.", platform.getName(), SdkConstants.FN_BUILD_PROP); } return null; } /** * Loads the Add-on from the SDK. * @param list the list to fill with the add-ons. * @param log the ISdkLog object receiving warning/error from the parsing. */ private void loadAddOns(ArrayList<IAndroidTarget> list, ISdkLog log) { File addonFolder = new File(mSdkLocation, SdkConstants.FD_ADDONS); if (addonFolder.isDirectory()) { File[] addons = addonFolder.listFiles(); for (File addon : addons) { // Add-ons have to be folders. Ignore files and no need to warn about them. if (addon.isDirectory()) { AddOnTarget target = loadAddon(addon, list, log); if (target != null) { list.add(target); } } } return; } String message = null; if (addonFolder.exists() == false) { message = "%s is missing."; } else { message = "%s is not a folder."; } throw new IllegalArgumentException(String.format(message, addonFolder.getAbsolutePath())); } /** * Loads a specific Add-on at a given location. * @param addon the location of the addon. * @param list * @param log */ private AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> list, ISdkLog log) { File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI); if (addOnManifest.isFile()) { Map<String, String> propertyMap = parsePropertyFile(addOnManifest, log); if (propertyMap != null) { // look for some specific values in the map. // we require name, vendor, and api String name = propertyMap.get(ADDON_NAME); if (name == null) { displayAddonManifestError(log, addon.getName(), ADDON_NAME); return null; } String vendor = propertyMap.get(ADDON_VENDOR); if (vendor == null) { displayAddonManifestError(log, addon.getName(), ADDON_VENDOR); return null; } String api = propertyMap.get(ADDON_API); PlatformTarget baseTarget = null; if (api == null) { displayAddonManifestError(log, addon.getName(), ADDON_API); return null; } else { try { int apiValue = Integer.parseInt(api); for (IAndroidTarget target : list) { if (target.isPlatform() && target.getApiVersionNumber() == apiValue) { baseTarget = (PlatformTarget)target; break; } } if (baseTarget == null) { if (log != null) { log.error(null, "Ignoring add-on '%1$s': Unable to find base platform with API level %2$d", addon.getName(), apiValue); } return null; } } catch (NumberFormatException e) { // looks like apiNumber does not parse to a number. // Ignore this add-on. if (log != null) { log.error(null, "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.", addon.getName(), ADDON_API, SdkConstants.FN_BUILD_PROP); } return null; } } // get the optional description String description = propertyMap.get(ADDON_DESCRIPTION); // get the optional libraries String librariesValue = propertyMap.get(ADDON_LIBRARIES); Map<String, String[]> libMap = null; if (librariesValue != null) { librariesValue = librariesValue.trim(); if (librariesValue.length() > 0) { // split in the string into the libraries name String[] libraries = librariesValue.split(";"); if (libraries.length > 0) { libMap = new HashMap<String, String[]>(); for (String libName : libraries) { libName = libName.trim(); // get the library data from the properties String libData = propertyMap.get(libName); if (libData != null) { // split the jar file from the description Matcher m = PATTERN_LIB_DATA.matcher(libData); if (m.matches()) { libMap.put(libName, new String[] { m.group(1), m.group(2) }); } else if (log != null) { log.error(null, "Ignoring library '%1$s', property value has wrong format\n\t%2$s", libName, libData); } } else if (log != null) { log.error(null, "Ignoring library '%1$s', missing property value", libName, libData); } } } } } AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor, description, libMap, baseTarget); // need to parse the skins. String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); // get the default skin, or take it from the base platform if needed. String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN); if (defaultSkin == null) { if (skins.length == 1) { defaultSkin = skins[1]; } else { defaultSkin = baseTarget.getDefaultSkin(); } } target.setSkins(skins, defaultSkin); // get the USB ID (if available) int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR)); if (usbVendorId != IAndroidTarget.NO_USB_ID) { target.setUsbVendorId(usbVendorId); } return target; } } else if (log != null) { log.error(null, "Ignoring add-on '%1$s': %2$s is missing.", addon.getName(), SdkConstants.FN_MANIFEST_INI); } return null; } /** * Converts a string representation of an hexadecimal ID into an int. * @param value the string to convert. * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed. */ private int convertId(String value) { if (value != null && value.length() > 0) { if (PATTERN_USB_IDS.matcher(value).matches()) { String v = value.substring(2); try { return Integer.parseInt(v, 16); } catch (NumberFormatException e) { // this shouldn't happen since we check the pattern above, but this is safer. // the method will return 0 below. } } } return IAndroidTarget.NO_USB_ID; } private void displayAddonManifestError(ISdkLog log, String addonName, String valueName) { if (log != null) { log.error(null, "Ignoring add-on '%1$s': '%2$s' is missing from %3$s.", addonName, valueName, SdkConstants.FN_MANIFEST_INI); } } /** * Checks the given platform has all the required files, and returns true if they are all * present. * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), * aidl(.exe), dx(.bat), and dx.jar */ private boolean checkPlatformContent(File platform, ISdkLog log) { for (String relativePath : sPlatformContentList) { File f = new File(platform, relativePath); if (f.exists() == false) { log.error(null, "Ignoring platform '%1$s': %2$s is missing.", platform.getName(), relativePath); return false; } } return true; } /** * Parses a property file (using UTF-8 encoding) and returns a map of the values, or null. * @param buildProp the property file to parse * @param log the ISdkLog object receiving warning/error from the parsing. * @return the map of (key,value) pairs, or null if the parsing failed. */ public static Map<String, String> parsePropertyFile(File buildProp, ISdkLog log) { FileInputStream fis = null; BufferedReader reader = null; try { fis = new FileInputStream(buildProp); reader = new BufferedReader(new InputStreamReader(fis, SdkConstants.INI_CHARSET)); String line = null; Map<String, String> map = new HashMap<String, String>(); while ((line = reader.readLine()) != null) { if (line.length() > 0 && line.charAt(0) != '#') { Matcher m = PATTERN_PROP.matcher(line); if (m.matches()) { map.put(m.group(1), m.group(2)); } else { log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", buildProp.getAbsolutePath(), line); return null; } } } return map; } catch (FileNotFoundException e) { // this should not happen since we usually test the file existence before // calling the method. // Return null below. } catch (IOException e) { if (log != null) { log.warning("Error parsing '%1$s': %2$s.", buildProp.getAbsolutePath(), e.getMessage()); } } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { // pass } } if (fis != null) { try { fis.close(); } catch (IOException e) { // pass } } } return null; } /** * Parses the skin folder and builds the skin list. * @param osPath The path of the skin root folder. */ private String[] parseSkinFolder(String osPath) { File skinRootFolder = new File(osPath); if (skinRootFolder.isDirectory()) { ArrayList<String> skinList = new ArrayList<String>(); File[] files = skinRootFolder.listFiles(); for (File skinFolder : files) { if (skinFolder.isDirectory()) { // check for layout file File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT); if (layout.isFile()) { // for now we don't parse the content of the layout and // simply add the directory to the list. skinList.add(skinFolder.getName()); } } } return skinList.toArray(new String[skinList.size()]); } return new String[0]; } }