/*
* 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.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.annotations.VisibleForTesting.Visibility;
import com.android.io.FileWrapper;
import com.android.prefs.AndroidLocation;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.AndroidVersion.AndroidVersionException;
import com.android.sdklib.ISystemImage.LocationType;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.repository.LocalSdkParser;
import com.android.sdklib.internal.repository.NullTaskMonitor;
import com.android.sdklib.internal.repository.archives.Archive;
import com.android.sdklib.internal.repository.packages.ExtraPackage;
import com.android.sdklib.internal.repository.packages.Package;
import com.android.sdklib.internal.repository.packages.PlatformToolPackage;
import com.android.sdklib.repository.PkgProps;
import com.android.utils.ILogger;
import com.android.utils.NullLogger;
import com.android.utils.Pair;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.Adler32;
/**
* The SDK manager parses the SDK folder and gives access to the content.
* @see PlatformTarget
* @see AddOnTarget
*/
public class SdkManager {
private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG") != null; //$NON-NLS-1$
public final static String PROP_VERSION_SDK = "ro.build.version.sdk"; //$NON-NLS-1$
public final static String PROP_VERSION_CODENAME = "ro.build.version.codename"; //$NON-NLS-1$
public final static String PROP_VERSION_RELEASE = "ro.build.version.release"; //$NON-NLS-1$
public final static String ADDON_NAME = "name"; //$NON-NLS-1$
public final static String ADDON_VENDOR = "vendor"; //$NON-NLS-1$
public final static String ADDON_API = "api"; //$NON-NLS-1$
public final static String ADDON_DESCRIPTION = "description"; //$NON-NLS-1$
public final static String ADDON_LIBRARIES = "libraries"; //$NON-NLS-1$
public final static String ADDON_DEFAULT_SKIN = "skin"; //$NON-NLS-1$
public final static String ADDON_USB_VENDOR = "usb-vendor"; //$NON-NLS-1$
public final static String ADDON_REVISION = "revision"; //$NON-NLS-1$
public final static String ADDON_REVISION_OLD = "version"; //$NON-NLS-1$
private final static Pattern PATTERN_LIB_DATA = Pattern.compile(
"^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
// usb ids are 16-bit hexadecimal values.
private final static Pattern PATTERN_USB_IDS = Pattern.compile(
"^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
/** 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,
};
/** Preference file containing the usb ids for adb */
private final static String ADB_INI_FILE = "adb_usb.ini"; //$NON-NLS-1$
//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" + //$NON-NLS-1$
"# USE 'android update adb' TO GENERATE.\n" + //$NON-NLS-1$
"# 1 USB VENDOR ID PER LINE.\n"; //$NON-NLS-1$
/** The location of the SDK as an OS path */
private final String mOsSdkPath;
/** Valid targets that have been loaded. Can be empty but not null. */
private IAndroidTarget[] mTargets = new IAndroidTarget[0];
/** A map to keep information on directories to see if they change later. */
private final Map<File, DirInfo> mTargetDirs = new HashMap<File, SdkManager.DirInfo>();
/**
* Create a new {@link SdkManager} instance.
* External users should use {@link #createManager(String, ILogger)}.
*
* @param osSdkPath the location of the SDK.
*/
@VisibleForTesting(visibility=Visibility.PRIVATE)
protected SdkManager(String osSdkPath) {
mOsSdkPath = osSdkPath;
}
/**
* Creates an {@link SdkManager} for a given sdk location.
* @param osSdkPath the location of the SDK.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
* @return the created {@link SdkManager} or null if the location is not valid.
*/
public static SdkManager createManager(String osSdkPath, ILogger log) {
try {
SdkManager manager = new SdkManager(osSdkPath);
manager.reloadSdk(log);
return manager;
} catch (IllegalArgumentException e) {
log.error(e, "Error parsing the sdk.");
}
return null;
}
/**
* Reloads the content of the SDK.
*
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
*/
public void reloadSdk(ILogger log) {
// get the current target list.
mTargetDirs.clear();
ArrayList<IAndroidTarget> targets = new ArrayList<IAndroidTarget>();
loadPlatforms(mOsSdkPath, targets, mTargetDirs, log);
loadAddOns(mOsSdkPath, targets, mTargetDirs, 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(targets);
setTargets(targets.toArray(new IAndroidTarget[targets.size()]));
// load the samples, after the targets have been set.
initializeSamplePaths(log);
}
/**
* Checks whether any of the SDK platforms/add-ons have changed on-disk
* since we last loaded the SDK. This does not reload the SDK nor does it
* change the underlying targets.
*
* @return True if at least one directory or source.prop has changed.
*/
public boolean hasChanged() {
Set<File> visited = new HashSet<File>();
boolean changed = false;
File platformFolder = new File(mOsSdkPath, SdkConstants.FD_PLATFORMS);
if (platformFolder.isDirectory()) {
File[] platforms = platformFolder.listFiles();
if (platforms != null) {
for (File platform : platforms) {
if (!platform.isDirectory()) {
continue;
}
visited.add(platform);
DirInfo dirInfo = mTargetDirs.get(platform);
if (dirInfo == null) {
// This is a new platform directory.
changed = true;
} else {
changed = dirInfo.hasChanged();
}
if (changed) {
if (DEBUG) {
System.out.println("SDK changed due to " + //$NON-NLS-1$
(dirInfo != null ? dirInfo.toString() : platform.getPath()));
}
}
}
}
}
File addonFolder = new File(mOsSdkPath, SdkConstants.FD_ADDONS);
if (!changed && addonFolder.isDirectory()) {
File[] addons = addonFolder.listFiles();
if (addons != null) {
for (File addon : addons) {
if (!addon.isDirectory()) {
continue;
}
visited.add(addon);
DirInfo dirInfo = mTargetDirs.get(addon);
if (dirInfo == null) {
// This is a new add-on directory.
changed = true;
} else {
changed = dirInfo.hasChanged();
}
if (changed) {
if (DEBUG) {
System.out.println("SDK changed due to " + //$NON-NLS-1$
(dirInfo != null ? dirInfo.toString() : addon.getPath()));
}
}
}
}
}
if (!changed) {
// Check whether some pre-existing target directories have vanished.
for (File previousDir : mTargetDirs.keySet()) {
if (!visited.contains(previousDir)) {
// This directory is no longer present.
changed = true;
if (DEBUG) {
System.out.println("SDK changed: " + //$NON-NLS-1$
previousDir.getPath() + " removed"); //$NON-NLS-1$
}
break;
}
}
}
return changed;
}
/**
* Returns the location of the SDK.
*/
public String getLocation() {
return mOsSdkPath;
}
/**
* Returns the targets that are available in the SDK.
* <p/>
* The array can be empty but not null.
*/
public IAndroidTarget[] getTargets() {
return mTargets;
}
/**
* Sets the targets that are available in the SDK.
* <p/>
* The array can be empty but not null.
*/
@VisibleForTesting(visibility=Visibility.PRIVATE)
protected void setTargets(IAndroidTarget[] targets) {
assert targets != null;
mTargets = targets;
}
/**
* 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)); //$NON-NLS-1$
}
} finally {
if (writer != null) {
writer.close();
}
}
}
/**
* Returns the greatest {@link LayoutlibVersion} found amongst all platform
* targets currently loaded in the SDK.
* <p/>
* We only started recording Layoutlib Versions recently in the platform meta data
* so it's possible to have an SDK with many platforms loaded but no layoutlib
* version defined.
*
* @return The greatest {@link LayoutlibVersion} or null if none is found.
* @deprecated This does NOT solve the right problem and will be changed later.
*/
@Deprecated
public LayoutlibVersion getMaxLayoutlibVersion() {
LayoutlibVersion maxVersion = null;
for (IAndroidTarget target : getTargets()) {
if (target instanceof PlatformTarget) {
LayoutlibVersion lv = ((PlatformTarget) target).getLayoutlibVersion();
if (lv != null) {
if (maxVersion == null || lv.compareTo(maxVersion) > 0) {
maxVersion = lv;
}
}
}
}
return maxVersion;
}
/**
* Returns a map of the <em>root samples directories</em> located in the SDK/extras packages.
* No guarantee is made that the extras' samples directory actually contain any valid samples.
* The only guarantee is that the root samples directory actually exists.
* The map is { File: Samples root directory => String: Extra package display name. }
*
* @return A non-null possibly empty map of extra samples directories and their associated
* extra package display name.
*/
public @NonNull Map<File, String> getExtraSamples() {
LocalSdkParser parser = new LocalSdkParser();
Package[] packages = parser.parseSdk(mOsSdkPath,
this,
LocalSdkParser.PARSE_EXTRAS,
new NullTaskMonitor(NullLogger.getLogger()));
Map<File, String> samples = new HashMap<File, String>();
for (Package pkg : packages) {
if (pkg instanceof ExtraPackage && pkg.isLocal()) {
// isLocal()==true implies there's a single locally-installed archive.
assert pkg.getArchives() != null && pkg.getArchives().length == 1;
Archive a = pkg.getArchives()[0];
assert a != null;
File path = new File(a.getLocalOsPath(), SdkConstants.FD_SAMPLES);
if (path.isDirectory()) {
samples.put(path, pkg.getListDescription());
continue;
}
// Some old-style extras simply have a single "sample" directory.
// Accept it if it contains an AndroidManifest.xml.
path = new File(a.getLocalOsPath(), SdkConstants.FD_SAMPLE);
if (path.isDirectory() &&
new File(path, SdkConstants.FN_ANDROID_MANIFEST_XML).isFile()) {
samples.put(path, pkg.getListDescription());
}
}
}
return samples;
}
/**
* Returns a map of all the extras found in the <em>local</em> SDK with their major revision.
* <p/>
* Map keys are in the form "vendor-id/path-id". These ids uniquely identify an extra package.
* The version is the incremental integer major revision of the package.
*
* @return A non-null possibly empty map of { string "vendor/path" => integer major revision }
*/
public @NonNull Map<String, Integer> getExtrasVersions() {
LocalSdkParser parser = new LocalSdkParser();
Package[] packages = parser.parseSdk(mOsSdkPath,
this,
LocalSdkParser.PARSE_EXTRAS,
new NullTaskMonitor(NullLogger.getLogger()));
Map<String, Integer> extraVersions = new TreeMap<String, Integer>();
for (Package pkg : packages) {
if (pkg instanceof ExtraPackage && pkg.isLocal()) {
ExtraPackage ep = (ExtraPackage) pkg;
String vendor = ep.getVendorId();
String path = ep.getPath();
int majorRev = ep.getRevision().getMajor();
extraVersions.put(vendor + '/' + path, majorRev);
}
}
return extraVersions;
}
/** Returns the platform tools version if installed, null otherwise. */
public @Nullable String getPlatformToolsVersion() {
LocalSdkParser parser = new LocalSdkParser();
Package[] packages = parser.parseSdk(mOsSdkPath, this, LocalSdkParser.PARSE_PLATFORM_TOOLS,
new NullTaskMonitor(NullLogger.getLogger()));
for (Package pkg : packages) {
if (pkg instanceof PlatformToolPackage && pkg.isLocal()) {
return pkg.getRevision().toShortString();
}
}
return null;
}
// -------- private methods ----------
/**
* Loads the Platforms from the SDK.
* Creates the "platforms" folder if necessary.
*
* @param sdkOsPath Location of the SDK
* @param targets the list to fill with the platforms.
* @param dirInfos a map to keep information on directories to see if they change later.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
* @throws RuntimeException when the "platforms" folder is missing and cannot be created.
*/
private static void loadPlatforms(
String sdkOsPath,
ArrayList<IAndroidTarget> targets,
Map<File, DirInfo> dirInfos, ILogger log) {
File platformFolder = new File(sdkOsPath, SdkConstants.FD_PLATFORMS);
if (platformFolder.isDirectory()) {
File[] platforms = platformFolder.listFiles();
for (File platform : platforms) {
PlatformTarget target = null;
if (platform.isDirectory()) {
target = loadPlatform(sdkOsPath, platform, log);
if (target != null) {
targets.add(target);
}
// Remember we visited this file/directory,
// even if we failed to load anything from it.
dirInfos.put(platform, new DirInfo(platform));
} else {
log.warning("Ignoring platform '%1$s', not a folder.", platform.getName());
}
}
return;
}
// Try to create it or complain if something else is in the way.
if (!platformFolder.exists()) {
if (!platformFolder.mkdir()) {
throw new RuntimeException(
String.format("Failed to create %1$s.",
platformFolder.getAbsolutePath()));
}
} else {
throw new RuntimeException(
String.format("%1$s is not a folder.",
platformFolder.getAbsolutePath()));
}
}
/**
* Loads a specific Platform at a given location.
* @param sdkOsPath Location of the SDK
* @param platformFolder the root folder of the platform.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
*/
private static PlatformTarget loadPlatform(
String sdkOsPath,
File platformFolder,
ILogger log) {
FileWrapper buildProp = new FileWrapper(platformFolder, SdkConstants.FN_BUILD_PROP);
FileWrapper sourcePropFile = new FileWrapper(platformFolder, SdkConstants.FN_SOURCE_PROP);
if (buildProp.isFile() && sourcePropFile.isFile()) {
Map<String, String> platformProp = new HashMap<String, String>();
// add all the property files
Map<String, String> map = ProjectProperties.parsePropertyFile(buildProp, log);
if (map != null) {
platformProp.putAll(map);
}
map = ProjectProperties.parsePropertyFile(sourcePropFile, log);
if (map != null) {
platformProp.putAll(map);
}
FileWrapper sdkPropFile = new FileWrapper(platformFolder, SdkConstants.FN_SDK_PROP);
if (sdkPropFile.isFile()) { // obsolete platforms don't have this.
map = ProjectProperties.parsePropertyFile(sdkPropFile, log);
if (map != null) {
platformProp.putAll(map);
}
}
// look for some specific values in the map.
// api level
int apiNumber;
String stringValue = platformProp.get(PROP_VERSION_SDK);
if (stringValue == null) {
log.warning(
"Ignoring platform '%1$s': %2$s is missing from '%3$s'",
platformFolder.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.
log.warning(
"Ignoring platform '%1$s': %2$s is not a valid number in %3$s.",
platformFolder.getName(), PROP_VERSION_SDK,
SdkConstants.FN_BUILD_PROP);
return null;
}
}
// Codename must be either null or a platform codename.
// REL means it's a release version and therefore the codename should be null.
AndroidVersion apiVersion =
new AndroidVersion(apiNumber, platformProp.get(PROP_VERSION_CODENAME));
// version string
String apiName = platformProp.get(PkgProps.PLATFORM_VERSION);
if (apiName == null) {
apiName = platformProp.get(PROP_VERSION_RELEASE);
}
if (apiName == null) {
log.warning(
"Ignoring platform '%1$s': %2$s is missing from '%3$s'",
platformFolder.getName(), PROP_VERSION_RELEASE,
SdkConstants.FN_BUILD_PROP);
return null;
}
// platform rev number & layoutlib version are extracted from the source.properties
// saved by the SDK Manager when installing the package.
int revision = 1;
LayoutlibVersion layoutlibVersion = null;
try {
revision = Integer.parseInt(platformProp.get(PkgProps.PKG_REVISION));
} catch (NumberFormatException e) {
// do nothing, we'll keep the default value of 1.
}
try {
String propApi = platformProp.get(PkgProps.LAYOUTLIB_API);
String propRev = platformProp.get(PkgProps.LAYOUTLIB_REV);
int llApi = propApi == null ? LayoutlibVersion.NOT_SPECIFIED :
Integer.parseInt(propApi);
int llRev = propRev == null ? LayoutlibVersion.NOT_SPECIFIED :
Integer.parseInt(propRev);
if (llApi > LayoutlibVersion.NOT_SPECIFIED &&
llRev >= LayoutlibVersion.NOT_SPECIFIED) {
layoutlibVersion = new LayoutlibVersion(llApi, llRev);
}
} catch (NumberFormatException e) {
// do nothing, we'll ignore the layoutlib version if it's invalid
}
// api number and name look valid, perform a few more checks
if (checkPlatformContent(platformFolder, log) == false) {
return null;
}
ISystemImage[] systemImages =
getPlatformSystemImages(sdkOsPath, platformFolder, apiVersion);
// create the target.
PlatformTarget target = new PlatformTarget(
sdkOsPath,
platformFolder.getAbsolutePath(),
apiVersion,
apiName,
revision,
layoutlibVersion,
systemImages,
platformProp);
// need to parse the skins.
String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
target.setSkins(skins);
return target;
} else {
log.warning("Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$
platformFolder.getName(),
SdkConstants.FN_BUILD_PROP);
}
return null;
}
/**
* Get all the system images supported by an add-on target.
* For an add-on, we first look for sub-folders in the addon/images directory.
* If none are found but the directory exists and is not empty, assume it's a legacy
* arm eabi system image.
* <p/>
* Note that it's OK for an add-on to have no system-images at all, since it can always
* rely on the ones from its base platform.
*
* @param root Root of the add-on target being loaded.
* @return an array of ISystemImage containing all the system images for the target.
* The list can be empty.
*/
private static ISystemImage[] getAddonSystemImages(File root) {
Set<ISystemImage> found = new TreeSet<ISystemImage>();
root = new File(root, SdkConstants.OS_IMAGES_FOLDER);
File[] files = root.listFiles();
boolean hasImgFiles = false;
if (files != null) {
// Look for sub-directories
for (File file : files) {
if (file.isDirectory()) {
found.add(new SystemImage(
file,
LocationType.IN_PLATFORM_SUBFOLDER,
file.getName()));
} else if (!hasImgFiles && file.isFile()) {
if (file.getName().endsWith(".img")) { //$NON-NLS-1$
hasImgFiles = true;
}
}
}
}
if (found.size() == 0 && hasImgFiles && root.isDirectory()) {
// We found no sub-folder system images but it looks like the top directory
// has some img files in it. It must be a legacy ARM EABI system image folder.
found.add(new SystemImage(
root,
LocationType.IN_PLATFORM_LEGACY,
SdkConstants.ABI_ARMEABI));
}
return found.toArray(new ISystemImage[found.size()]);
}
/**
* Get all the system images supported by a platform target.
* For a platform, we first look in the new sdk/system-images folders then we
* look for sub-folders in the platform/images directory and/or the one legacy
* folder.
* If any given API appears twice or more, the first occurrence wins.
*
* @param sdkOsPath The path to the SDK.
* @param root Root of the platform target being loaded.
* @param version API level + codename of platform being loaded.
* @return an array of ISystemImage containing all the system images for the target.
* The list can be empty.
*/
private static ISystemImage[] getPlatformSystemImages(
String sdkOsPath,
File root,
AndroidVersion version) {
Set<ISystemImage> found = new TreeSet<ISystemImage>();
Set<String> abiFound = new HashSet<String>();
// First look in the SDK/system-image/platform-n/abi folders.
// We require/enforce the system image to have a valid properties file.
// The actual directory names are irrelevant.
// If we find multiple occurrences of the same platform/abi, the first one read wins.
File[] firstLevelFiles = new File(sdkOsPath, SdkConstants.FD_SYSTEM_IMAGES).listFiles();
if (firstLevelFiles != null) {
for (File firstLevel : firstLevelFiles) {
File[] secondLevelFiles = firstLevel.listFiles();
if (secondLevelFiles != null) {
for (File secondLevel : secondLevelFiles) {
try {
File propFile = new File(secondLevel, SdkConstants.FN_SOURCE_PROP);
Properties props = new Properties();
FileInputStream fis = null;
try {
fis = new FileInputStream(propFile);
props.load(fis);
} finally {
if (fis != null) {
fis.close();
}
}
AndroidVersion propsVersion = new AndroidVersion(props);
if (!propsVersion.equals(version)) {
continue;
}
String abi = props.getProperty(PkgProps.SYS_IMG_ABI);
if (abi != null && !abiFound.contains(abi)) {
found.add(new SystemImage(
secondLevel,
LocationType.IN_SYSTEM_IMAGE,
abi));
abiFound.add(abi);
}
} catch (Exception ignore) {
}
}
}
}
}
// Then look in either the platform/images/abi or the legacy folder
root = new File(root, SdkConstants.OS_IMAGES_FOLDER);
File[] files = root.listFiles();
boolean useLegacy = true;
boolean hasImgFiles = false;
if (files != null) {
// Look for sub-directories
for (File file : files) {
if (file.isDirectory()) {
useLegacy = false;
String abi = file.getName();
if (!abiFound.contains(abi)) {
found.add(new SystemImage(
file,
LocationType.IN_PLATFORM_SUBFOLDER,
abi));
abiFound.add(abi);
}
} else if (!hasImgFiles && file.isFile()) {
if (file.getName().endsWith(".img")) { //$NON-NLS-1$
hasImgFiles = true;
}
}
}
}
if (useLegacy && hasImgFiles && root.isDirectory() &&
!abiFound.contains(SdkConstants.ABI_ARMEABI)) {
// We found no sub-folder system images but it looks like the top directory
// has some img files in it. It must be a legacy ARM EABI system image folder.
found.add(new SystemImage(
root,
LocationType.IN_PLATFORM_LEGACY,
SdkConstants.ABI_ARMEABI));
}
return found.toArray(new ISystemImage[found.size()]);
}
/**
* Loads the Add-on from the SDK.
* Creates the "add-ons" folder if necessary.
*
* @param osSdkPath Location of the SDK
* @param targets the list to fill with the add-ons.
* @param dirInfos a map to keep information on directories to see if they change later.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
* @throws RuntimeException when the "add-ons" folder is missing and cannot be created.
*/
private static void loadAddOns(
String osSdkPath,
ArrayList<IAndroidTarget> targets,
Map<File, DirInfo> dirInfos, ILogger log) {
File addonFolder = new File(osSdkPath, SdkConstants.FD_ADDONS);
if (addonFolder.isDirectory()) {
File[] addons = addonFolder.listFiles();
IAndroidTarget[] targetList = targets.toArray(new IAndroidTarget[targets.size()]);
if (addons != null) {
for (File addon : addons) {
// Add-ons have to be folders. Ignore files and no need to warn about them.
AddOnTarget target = null;
if (addon.isDirectory()) {
target = loadAddon(addon, targetList, log);
if (target != null) {
targets.add(target);
}
// Remember we visited this file/directory,
// even if we failed to load anything from it.
dirInfos.put(addon, new DirInfo(addon));
}
}
}
return;
}
// Try to create it or complain if something else is in the way.
if (!addonFolder.exists()) {
if (!addonFolder.mkdir()) {
throw new RuntimeException(
String.format("Failed to create %1$s.",
addonFolder.getAbsolutePath()));
}
} else {
throw new RuntimeException(
String.format("%1$s is not a folder.",
addonFolder.getAbsolutePath()));
}
}
/**
* Loads a specific Add-on at a given location.
* @param addonDir the location of the add-on directory.
* @param targetList The list of Android target that were already loaded from the SDK.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
*/
private static AddOnTarget loadAddon(File addonDir,
IAndroidTarget[] targetList,
ILogger log) {
// Parse the addon properties to ensure we can load it.
Pair<Map<String, String>, String> infos = parseAddonProperties(addonDir, targetList, log);
Map<String, String> propertyMap = infos.getFirst();
String error = infos.getSecond();
if (error != null) {
log.warning("Ignoring add-on '%1$s': %2$s", addonDir.getName(), error);
return null;
}
// Since error==null we're not supposed to encounter any issues loading this add-on.
try {
assert propertyMap != null;
String api = propertyMap.get(ADDON_API);
String name = propertyMap.get(ADDON_NAME);
String vendor = propertyMap.get(ADDON_VENDOR);
assert api != null;
assert name != null;
assert vendor != null;
PlatformTarget baseTarget = null;
// 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;
}
}
assert baseTarget != 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) {
revisionValue = Integer.parseInt(revision);
}
// 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(";"); //$NON-NLS-1$
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 {
log.warning(
"Ignoring library '%1$s', property value has wrong format\n\t%2$s",
libName, libData);
}
} else {
log.warning(
"Ignoring library '%1$s', missing property value",
libName, libData);
}
}
}
}
}
// get the abi list.
ISystemImage[] systemImages = getAddonSystemImages(addonDir);
// check whether the add-on provides its own rendering info/library.
boolean hasRenderingLibrary = false;
boolean hasRenderingResources = false;
File dataFolder = new File(addonDir, SdkConstants.FD_DATA);
if (dataFolder.isDirectory()) {
hasRenderingLibrary = new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR).isFile();
hasRenderingResources = new File(dataFolder, SdkConstants.FD_RES).isDirectory() &&
new File(dataFolder, SdkConstants.FD_FONTS).isDirectory();
}
AddOnTarget target = new AddOnTarget(addonDir.getAbsolutePath(), name, vendor,
revisionValue, description, systemImages, libMap,
hasRenderingLibrary, hasRenderingResources,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;
}
catch (Exception e) {
log.warning("Ignoring add-on '%1$s': error %2$s.",
addonDir.getName(), e.toString());
}
return null;
}
/**
* Parses the add-on properties and decodes any error that occurs when loading an addon.
*
* @param addonDir the location of the addon directory.
* @param targetList The list of Android target that were already loaded from the SDK.
* @param log the ILogger object receiving warning/error from the parsing. Cannot be null.
* @return A pair with the property map and an error string. Both can be null but not at the
* same time. If a non-null error is present then the property map must be ignored. The error
* should be translatable as it might show up in the SdkManager UI.
*/
public static Pair<Map<String, String>, String> parseAddonProperties(
File addonDir,
IAndroidTarget[] targetList,
ILogger log) {
Map<String, String> propertyMap = null;
String error = null;
FileWrapper addOnManifest = new FileWrapper(addonDir, SdkConstants.FN_MANIFEST_INI);
do {
if (!addOnManifest.isFile()) {
error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI);
break;
}
propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, log);
if (propertyMap == null) {
error = String.format("Failed to parse properties from %1$s",
SdkConstants.FN_MANIFEST_INI);
break;
}
// look for some specific values in the map.
// we require name, vendor, and api
String name = propertyMap.get(ADDON_NAME);
if (name == null) {
error = addonManifestWarning(ADDON_NAME);
break;
}
String vendor = propertyMap.get(ADDON_VENDOR);
if (vendor == null) {
error = addonManifestWarning(ADDON_VENDOR);
break;
}
String api = propertyMap.get(ADDON_API);
PlatformTarget baseTarget = null;
if (api == null) {
error = addonManifestWarning(ADDON_API);
break;
}
// 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) {
error = String.format("Unable to find base platform with API level '%1$s'", api);
break;
}
// get the add-on revision
String revision = propertyMap.get(ADDON_REVISION);
if (revision == null) {
revision = propertyMap.get(ADDON_REVISION_OLD);
}
if (revision != null) {
try {
Integer.parseInt(revision);
} catch (NumberFormatException e) {
// looks like revision does not parse to a number.
error = String.format("%1$s is not a valid number in %2$s.",
ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
break;
}
}
} while(false);
return Pair.of(propertyMap, error);
}
/**
* 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;
}
/**
* Prepares a warning about the addon being ignored due to a missing manifest value.
* This string will show up in the SdkManager UI.
*
* @param valueName The missing manifest value, for display.
*/
private static String addonManifestWarning(String valueName) {
return String.format("'%1$s' is missing from %2$s.",
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
*
* @param platform The folder containing the platform.
* @param log Logger. Cannot be null.
*/
private static boolean checkPlatformContent(File platform, ILogger log) {
for (String relativePath : sPlatformContentList) {
File f = new File(platform, relativePath);
if (!f.exists()) {
log.warning(
"Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$
platform.getName(), relativePath);
return false;
}
}
return true;
}
/**
* 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];
}
/**
* Initialize the sample folders for all known targets (platforms and addons).
* <p/>
* Samples used to be located at SDK/Target/samples. We then changed this to
* have a separate SDK/samples/samples-API directory. This parses either directories
* and sets the targets' sample path accordingly.
*
* @param log Logger. Cannot be null.
*/
private void initializeSamplePaths(ILogger log) {
File sampleFolder = new File(mOsSdkPath, SdkConstants.FD_SAMPLES);
if (sampleFolder.isDirectory()) {
File[] platforms = sampleFolder.listFiles();
for (File platform : platforms) {
if (platform.isDirectory()) {
// load the source.properties file and get an AndroidVersion object from it.
AndroidVersion version = getSamplesVersion(platform, log);
if (version != null) {
// locate the platform matching this version
for (IAndroidTarget target : mTargets) {
if (target.isPlatform() && target.getVersion().equals(version)) {
((PlatformTarget)target).setSamplesPath(platform.getAbsolutePath());
break;
}
}
}
}
}
}
}
/**
* Returns the {@link AndroidVersion} of the sample in the given folder.
*
* @param folder The sample's folder.
* @param log Logger for errors. Cannot be null.
* @return An {@link AndroidVersion} or null on error.
*/
private AndroidVersion getSamplesVersion(File folder, ILogger log) {
File sourceProp = new File(folder, SdkConstants.FN_SOURCE_PROP);
try {
Properties p = new Properties();
FileInputStream fis = null;
try {
fis = new FileInputStream(sourceProp);
p.load(fis);
} finally {
if (fis != null) {
fis.close();
}
}
return new AndroidVersion(p);
} catch (FileNotFoundException e) {
log.warning("Ignoring sample '%1$s': does not contain %2$s.", //$NON-NLS-1$
folder.getName(), SdkConstants.FN_SOURCE_PROP);
} catch (IOException e) {
log.warning("Ignoring sample '%1$s': failed reading %2$s.", //$NON-NLS-1$
folder.getName(), SdkConstants.FN_SOURCE_PROP);
} catch (AndroidVersionException e) {
log.warning("Ignoring sample '%1$s': no android version found in %2$s.", //$NON-NLS-1$
folder.getName(), SdkConstants.FN_SOURCE_PROP);
}
return null;
}
// -------------
public static class LayoutlibVersion implements Comparable<LayoutlibVersion> {
private final int mApi;
private final int mRevision;
public static final int NOT_SPECIFIED = 0;
public LayoutlibVersion(int api, int revision) {
mApi = api;
mRevision = revision;
}
public int getApi() {
return mApi;
}
public int getRevision() {
return mRevision;
}
@Override
public int compareTo(LayoutlibVersion rhs) {
boolean useRev = this.mRevision > NOT_SPECIFIED && rhs.mRevision > NOT_SPECIFIED;
int lhsValue = (this.mApi << 16) + (useRev ? this.mRevision : 0);
int rhsValue = (rhs.mApi << 16) + (useRev ? rhs.mRevision : 0);
return lhsValue - rhsValue;
}
}
// -------------
private static class DirInfo {
private final @NonNull File mDir;
private final long mDirModifiedTS;
private final long mPropsModifedTS;
private final long mPropsChecksum;
/**
* Creates a new immutable {@link DirInfo}.
*
* @param dir The platform/addon directory of the target. It should be a directory.
*/
public DirInfo(@NonNull File dir) {
mDir = dir;
mDirModifiedTS = dir.lastModified();
// Capture some info about the source.properties file if it exists.
// We use propsModifedTS == 0 to mean there is no props file.
long propsChecksum = 0;
long propsModifedTS = 0;
File props = new File(dir, SdkConstants.FN_SOURCE_PROP);
if (props.isFile()) {
propsModifedTS = props.lastModified();
propsChecksum = getFileChecksum(props);
}
mPropsModifedTS = propsModifedTS;
mPropsChecksum = propsChecksum;
}
/**
* Checks whether the directory/source.properties attributes have changed.
*
* @return True if the directory modified timestampd or
* its source.property files have changed.
*/
public boolean hasChanged() {
// Does platform directory still exist?
if (!mDir.isDirectory()) {
return true;
}
// Has platform directory modified-timestamp changed?
if (mDirModifiedTS != mDir.lastModified()) {
return true;
}
File props = new File(mDir, SdkConstants.FN_SOURCE_PROP);
// The directory did not have a props file if target was null or
// if mPropsModifedTS is 0.
boolean hadProps = mPropsModifedTS != 0;
// Was there a props file and it vanished, or there wasn't and there's one now?
if (hadProps != props.isFile()) {
return true;
}
if (hadProps) {
// Has source.props file modified-timestampd changed?
if (mPropsModifedTS != props.lastModified()) {
return true;
}
// Had the content of source.props changed?
if (mPropsChecksum != getFileChecksum(props)) {
return true;
}
}
return false;
}
/**
* Computes an adler32 checksum (source.props are small files, so this
* should be OK with an acceptable collision rate.)
*/
private static long getFileChecksum(File file) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
Adler32 a = new Adler32();
byte[] buf = new byte[1024];
int n;
while ((n = fis.read(buf)) > 0) {
a.update(buf, 0, n);
}
return a.getValue();
} catch (Exception ignore) {
} finally {
try {
if (fis != null) {
fis.close();
}
} catch(Exception ignore) {};
}
return 0;
}
/** Returns a visual representation of this object for debugging. */
@Override
public String toString() {
String s = String.format("<DirInfo %1$s TS=%2$d", mDir, mDirModifiedTS); //$NON-NLS-1$
if (mPropsModifedTS != 0) {
s += String.format(" | Props TS=%1$d, Chksum=%2$s", //$NON-NLS-1$
mPropsModifedTS, mPropsChecksum);
}
return s + ">"; //$NON-NLS-1$
}
}
}