blob: 7dda92123bad5035ee95f8d0cae8fb7f545d3288 [file] [log] [blame]
/*
* Copyright (C) 2013 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.repository.local;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISystemImage;
import com.android.sdklib.ISystemImage.LocationType;
import com.android.sdklib.SystemImage;
import com.android.sdklib.internal.androidTarget.AddOnTarget;
import com.android.sdklib.internal.androidTarget.PlatformTarget;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.io.FileOp;
import com.android.sdklib.io.IFileOp;
import com.android.sdklib.repository.AddonManifestIniProps;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.MajorRevision;
import com.android.sdklib.repository.descriptors.*;
import com.android.utils.Pair;
import com.google.common.base.Objects;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.TreeMultimap;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings("MethodMayBeStatic")
public class LocalAddonPkgInfo extends LocalPlatformPkgInfo {
private static final 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 static final Pattern PATTERN_USB_IDS = Pattern.compile(
"^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
@NonNull
private final IPkgDescAddon mAddonDesc;
public LocalAddonPkgInfo(@NonNull LocalSdk localSdk,
@NonNull File localDir,
@NonNull Properties sourceProps,
@NonNull AndroidVersion version,
@NonNull MajorRevision revision,
@NonNull IdDisplay vendor,
@NonNull IdDisplay name) {
super(localSdk, localDir, sourceProps, version, revision, FullRevision.NOT_SPECIFIED);
mAddonDesc = (IPkgDescAddon) PkgDesc.Builder.newAddon(version, revision, vendor, name)
.create();
}
@NonNull
@Override
public IPkgDesc getDesc() {
return mAddonDesc;
}
/** The "path" of an add-on is its Target Hash. */
@Override
@NonNull
public String getTargetHash() {
return getDesc().getPath();
}
//-----
/**
* Computes a sanitized name-id based on an addon name-display.
* This is used to provide compatibility with older add-ons that lacks the new fields.
*
* @param displayName A name-display field or a old-style name field.
* @return A non-null sanitized name-id that fits in the {@code [a-zA-Z0-9_-]+} pattern.
*/
public static String sanitizeDisplayToNameId(@NonNull String displayName) {
String name = displayName.toLowerCase(Locale.US);
name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
// Trim leading and trailing underscores
if (name.length() > 1) {
name = name.replaceAll("^_+", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
if (name.length() > 1) {
name = name.replaceAll("_+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
return name;
}
//-----
/**
* Creates the AddOnTarget. Invoked by {@link #getAndroidTarget()}.
*/
@Override
@Nullable
protected IAndroidTarget createAndroidTarget() {
LocalSdk sdk = getLocalSdk();
IFileOp fileOp = sdk.getFileOp();
// Parse the addon properties to ensure we can load it.
Pair<Map<String, String>, String> infos = parseAddonProperties();
Map<String, String> propertyMap = infos.getFirst();
String error = infos.getSecond();
if (error != null) {
appendLoadError("Ignoring add-on '%1$s': %2$s", getLocalDir().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(AddonManifestIniProps.ADDON_API);
String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME);
String vendor = propertyMap.get(AddonManifestIniProps.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.
LocalPkgInfo plat = sdk.getPkgInfo(PkgType.PKG_PLATFORM,
getDesc().getAndroidVersion());
if (plat instanceof LocalPlatformPkgInfo) {
baseTarget = (PlatformTarget) ((LocalPlatformPkgInfo) plat).getAndroidTarget();
}
assert baseTarget != null;
// get the optional description
String description = propertyMap.get(AddonManifestIniProps.ADDON_DESCRIPTION);
// get the add-on revision
int revisionValue = 1;
String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION);
if (revision == null) {
revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD);
}
if (revision != null) {
revisionValue = Integer.parseInt(revision);
}
// get the optional libraries
String librariesValue = propertyMap.get(AddonManifestIniProps.ADDON_LIBRARIES);
Map<String, String[]> libMap = null;
if (librariesValue != null) {
librariesValue = librariesValue.trim();
if (!librariesValue.isEmpty()) {
// 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 {
appendLoadError(
"Ignoring library '%1$s', property value has wrong format\n\t%2$s",
libName, libData);
}
} else {
appendLoadError(
"Ignoring library '%1$s', missing property value",
libName, libData);
}
}
}
}
}
// get the abi list.
ISystemImage[] systemImages = getAddonSystemImages(fileOp);
// check whether the add-on provides its own rendering info/library.
boolean hasRenderingLibrary = false;
boolean hasRenderingResources = false;
File dataFolder = new File(getLocalDir(), SdkConstants.FD_DATA);
if (fileOp.isDirectory(dataFolder)) {
hasRenderingLibrary =
fileOp.isFile(new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR));
hasRenderingResources =
fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_RES)) &&
fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_FONTS));
}
AddOnTarget target = new AddOnTarget(
getLocalDir().getAbsolutePath(),
name,
vendor,
revisionValue,
description,
systemImages,
libMap,
hasRenderingLibrary,
hasRenderingResources,
baseTarget);
// parse the legacy skins, located under SDK/addons/addon-name/skins/[skin-name]
// and merge with the system-image skins, if any, merging them by name.
File targetSkinFolder = target.getFile(IAndroidTarget.SKINS);
Map<String, File> skinsMap = new TreeMap<String, File>();
for (File f : PackageParserUtils.parseSkinFolder(targetSkinFolder, fileOp)) {
skinsMap.put(f.getName().toLowerCase(Locale.US), f);
}
for (ISystemImage si : systemImages) {
for (File f : si.getSkins()) {
skinsMap.put(f.getName().toLowerCase(Locale.US), f);
}
}
List<File> skins = new ArrayList<File>(skinsMap.values());
Collections.sort(skins);
// get the default skin
File defaultSkin = null;
String defaultSkinName = propertyMap.get(AddonManifestIniProps.ADDON_DEFAULT_SKIN);
if (defaultSkinName != null) {
defaultSkin = new File(targetSkinFolder, defaultSkinName);
} else {
// No default skin name specified, use the first one from the addon
// or the default from the platform.
if (skins.size() == 1) {
defaultSkin = skins.get(0);
} else {
defaultSkin = baseTarget.getDefaultSkin();
}
}
// get the USB ID (if available)
int usbVendorId = convertId(propertyMap.get(AddonManifestIniProps.ADDON_USB_VENDOR));
if (usbVendorId != IAndroidTarget.NO_USB_ID) {
target.setUsbVendorId(usbVendorId);
}
target.setSkins(skins.toArray(new File[skins.size()]), defaultSkin);
return target;
} catch (Exception e) {
appendLoadError("Ignoring add-on '%1$s': error %2$s.",
getLocalDir().getName(), e.toString());
}
return null;
}
/**
* Parses the add-on properties and decodes any error that occurs when loading an addon.
*
* @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.
*/
@NonNull
private Pair<Map<String, String>, String> parseAddonProperties() {
Map<String, String> propertyMap = null;
String error = null;
IFileOp fileOp = getLocalSdk().getFileOp();
File addOnManifest = new File(getLocalDir(), SdkConstants.FN_MANIFEST_INI);
do {
if (!fileOp.isFile(addOnManifest)) {
error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI);
break;
}
try {
propertyMap = ProjectProperties.parsePropertyStream(
fileOp.newFileInputStream(addOnManifest),
addOnManifest.getPath(),
null /*log*/);
if (propertyMap == null) {
error = String.format("Failed to parse properties from %1$s",
SdkConstants.FN_MANIFEST_INI);
break;
}
} catch (FileNotFoundException e) {
// this can happen if the system fails to open the file because of too many
// open files.
error = String.format("Failed to parse properties from %1$s: %2$s",
SdkConstants.FN_MANIFEST_INI, e.getMessage());
break;
}
// look for some specific values in the map.
// we require name, vendor, and api
String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME);
if (name == null) {
error = addonManifestWarning(AddonManifestIniProps.ADDON_NAME);
break;
}
String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR);
if (vendor == null) {
error = addonManifestWarning(AddonManifestIniProps.ADDON_VENDOR);
break;
}
String api = propertyMap.get(AddonManifestIniProps.ADDON_API);
if (api == null) {
error = addonManifestWarning(AddonManifestIniProps.ADDON_API);
break;
}
// Look for a platform that has a matching api level or codename.
IAndroidTarget baseTarget = null;
LocalPkgInfo plat = getLocalSdk().getPkgInfo(PkgType.PKG_PLATFORM,
getDesc().getAndroidVersion());
if (plat instanceof LocalPlatformPkgInfo) {
baseTarget = ((LocalPlatformPkgInfo) plat).getAndroidTarget();
}
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(AddonManifestIniProps.ADDON_REVISION);
if (revision == null) {
revision = propertyMap.get(AddonManifestIniProps.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.",
AddonManifestIniProps.ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
break;
}
}
} while(false);
return Pair.of(propertyMap, error);
}
/**
* 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.
*/
@NonNull
private static String addonManifestWarning(@NonNull String valueName) {
return String.format("'%1$s' is missing from %2$s.",
valueName, SdkConstants.FN_MANIFEST_INI);
}
/**
* 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 conversion failed.
*/
private int convertId(@Nullable String value) {
if (value != null && !value.isEmpty()) {
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;
}
/**
* Get all the system images supported by an add-on target.
* For an add-on, we first look in the new sdk/system-images folders then we 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.
* If any given API appears twice or more, the first occurrence wins.
* <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 fileOp File operation wrapper.
* @return an array of ISystemImage containing all the system images for the target.
* The list can be empty but not null.
*/
@NonNull
private ISystemImage[] getAddonSystemImages(IFileOp fileOp) {
Set<ISystemImage> found = new TreeSet<ISystemImage>();
SetMultimap<IdDisplay, String> tagToAbiFound = TreeMultimap.create();
// Look in the system images folders:
// - SDK/system-image/platform/addon-id-tag/abi
// - SDK/system-image/addon-id-tag/abi (many abi possible)
// Optional: look for skins under
// - SDK/system-image/platform/addon-id-tag/abi/skins/skin-name
// - SDK/system-image/addon-id-tag/abi/skins/skin-name
// If we find multiple occurrences of the same platform/abi, the first one read wins.
LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE);
for (LocalPkgInfo pkg : sysImgInfos) {
IPkgDesc d = pkg.getDesc();
if (pkg instanceof LocalAddonSysImgPkgInfo &&
d.hasVendor() &&
mAddonDesc.getVendor().equals(d.getVendor()) &&
mAddonDesc.getName().equals(d.getTag()) &&
Objects.equal(mAddonDesc.getAndroidVersion(), pkg.getDesc().getAndroidVersion())) {
final IdDisplay tag = mAddonDesc.getName();
final String abi = d.getPath();
if (abi != null && !tagToAbiFound.containsEntry(tag, abi)) {
found.add(((LocalAddonSysImgPkgInfo)pkg).getSystemImage());
tagToAbiFound.put(tag, abi);
}
}
}
// Look for sub-directories:
// - SDK/addons/addon-name/images/abi (multiple abi possible)
// - SDK/addons/addon-name/armeabi (legacy support)
boolean useLegacy = true;
boolean hasImgFiles = false;
final IdDisplay defaultTag = SystemImage.DEFAULT_TAG;
File imagesDir = new File(getLocalDir(), SdkConstants.OS_IMAGES_FOLDER);
File[] files = fileOp.listFiles(imagesDir);
for (File file : files) {
if (fileOp.isDirectory(file)) {
useLegacy = false;
String abi = file.getName();
if (!tagToAbiFound.containsEntry(defaultTag, abi)) {
found.add(new SystemImage(
file,
LocationType.IN_IMAGES_SUBFOLDER,
SystemImage.DEFAULT_TAG,
mAddonDesc.getVendor(),
abi,
FileOp.EMPTY_FILE_ARRAY));
tagToAbiFound.put(defaultTag, abi);
}
} else if (!hasImgFiles && fileOp.isFile(file)) {
if (file.getName().endsWith(".img")) { //$NON-NLS-1$
// The legacy images folder is only valid if it contains some .img files
hasImgFiles = true;
}
}
}
if (useLegacy &&
hasImgFiles &&
fileOp.isDirectory(imagesDir) &&
!tagToAbiFound.containsEntry(defaultTag, 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(
imagesDir,
LocationType.IN_LEGACY_FOLDER,
SystemImage.DEFAULT_TAG,
SdkConstants.ABI_ARMEABI,
FileOp.EMPTY_FILE_ARRAY));
}
return found.toArray(new ISystemImage[found.size()]);
}
}