blob: b23d1bd584d690f22a521de350cf98980d5d128e [file] [log] [blame]
/*
* 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_CODENAME = "ro.build.version.codename";
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 String ADDON_REVISION = "revision";
private final static String ADDON_REVISION_OLD = "version";
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>();
loadPlatforms(sdkLocation, list, log);
loadAddOns(sdkLocation, 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 {
if (writer != null) {
writer.close();
}
}
}
/**
* Reloads the content of the SDK.
* @param log the ISdkLog object receiving warning/error from the parsing.
*/
public void reloadSdk(ISdkLog log) {
// get the current target list.
ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
loadPlatforms(mSdkLocation, list, log);
loadAddOns(mSdkLocation, list, log);
// For now replace the old list with the new one.
// In the future we may want to keep the current objects, so that ADT doesn't have to deal
// with new IAndroidTarget objects when a target didn't actually change.
// sort the targets/add-ons
Collections.sort(list);
setTargets(list.toArray(new IAndroidTarget[list.size()]));
}
private SdkManager(String sdkLocation) {
mSdkLocation = sdkLocation;
}
private void setTargets(IAndroidTarget[] targets) {
mTargets = targets;
}
/**
* Loads the Platforms from the SDK.
* @param location Location of the SDK
* @param list the list to fill with the platforms.
* @param log the ISdkLog object receiving warning/error from the parsing.
*/
private static void loadPlatforms(String location, ArrayList<IAndroidTarget> list,
ISdkLog log) {
File platformFolder = new File(location, 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 static 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.
// version string
String apiName = map.get(PROP_VERSION_RELEASE);
if (apiName == null) {
if (log != null) {
log.error(null,
"Ignoring platform '%1$s': %2$s is missing from '%3$s'",
platform.getName(), PROP_VERSION_RELEASE,
SdkConstants.FN_BUILD_PROP);
}
return null;
}
// api level
int apiNumber;
String stringValue = map.get(PROP_VERSION_SDK);
if (stringValue == null) {
if (log != null) {
log.error(null,
"Ignoring platform '%1$s': %2$s is missing from '%3$s'",
platform.getName(), PROP_VERSION_SDK,
SdkConstants.FN_BUILD_PROP);
}
return null;
} else {
try {
apiNumber = Integer.parseInt(stringValue);
} 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);
}
return null;
}
}
// codename (optional)
String apiCodename = map.get(PROP_VERSION_CODENAME);
if (apiCodename != null && apiCodename.equals("REL")) {
apiCodename = null; // REL means it's a release version and therefore the
// codename is irrelevant at this point.
}
int revision = 1;
// FIXME: properly parse the platform revision.
// api number and name look valid, perform a few more checks
if (checkPlatformContent(platform, log) == false) {
return null;
}
// create the target.
PlatformTarget target = new PlatformTarget(
platform.getAbsolutePath(),
map,
apiNumber,
apiCodename,
apiName,
revision);
// need to parse the skins.
String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
target.setSkins(skins);
return target;
}
} 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 location Location of the SDK
* @param list the list to fill with the add-ons.
* @param log the ISdkLog object receiving warning/error from the parsing.
*/
private static void loadAddOns(String location, ArrayList<IAndroidTarget> list, ISdkLog log) {
File addonFolder = new File(location, 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 targetList The list of Android target that were already loaded from the SDK.
* @param log the ISdkLog object receiving warning/error from the parsing.
*/
private static AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> targetList,
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 {
// Look for a platform that has a matching api level or codename.
for (IAndroidTarget target : targetList) {
if (target.isPlatform() && target.getVersion().equals(api)) {
baseTarget = (PlatformTarget)target;
break;
}
}
if (baseTarget == null) {
// Ignore this add-on.
if (log != null) {
log.error(null,
"Ignoring add-on '%1$s': Unable to find base platform with API level '%2$s'",
addon.getName(), api);
}
return null;
}
}
// get the optional description
String description = propertyMap.get(ADDON_DESCRIPTION);
// get the add-on revision
int revisionValue = 1;
String revision = propertyMap.get(ADDON_REVISION);
if (revision == null) {
revision = propertyMap.get(ADDON_REVISION_OLD);
}
if (revision != null) {
try {
revisionValue = Integer.parseInt(revision);
} 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_REVISION, SdkConstants.FN_BUILD_PROP);
}
return null;
}
}
// 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,
revisionValue, 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[0];
} else {
defaultSkin = baseTarget.getDefaultSkin();
}
}
// get the USB ID (if available)
int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
if (usbVendorId != IAndroidTarget.NO_USB_ID) {
target.setUsbVendorId(usbVendorId);
}
target.setSkins(skins, defaultSkin);
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 static 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 static 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 static 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
* @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 static 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];
}
}